Skip to content

Commit 3a19009

Browse files
committed
feat(container): adds image pruning and build cache pruning features through the ui
1 parent 278c870 commit 3a19009

File tree

16 files changed

+589
-397
lines changed

16 files changed

+589
-397
lines changed

view/app/containers/[id]/components/ContainerDetailsLoading.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Card, CardContent } from '@/components/ui/card'
2-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
3-
import React from 'react'
4-
import { Skeleton } from '@/components/ui/skeleton'
1+
import { Card, CardContent } from '@/components/ui/card';
2+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
3+
import React from 'react';
4+
import { Skeleton } from '@/components/ui/skeleton';
55

66
function ContainerDetailsLoading() {
77
return (
@@ -92,7 +92,7 @@ function ContainerDetailsLoading() {
9292
</TabsContent>
9393
</Tabs>
9494
</div>
95-
)
95+
);
9696
}
9797

98-
export default ContainerDetailsLoading
98+
export default ContainerDetailsLoading;

view/app/containers/[id]/components/DetailsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ export function DetailsTab({ container }: DetailsTabProps) {
1313
</pre>
1414
</ScrollArea>
1515
);
16-
}
16+
}

view/app/containers/[id]/components/LogsTab.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,11 @@ export function LogsTab({ container, logs, onLoadMore }: LogsTabProps) {
2121
</pre>
2222
</ScrollArea>
2323
<div className="flex justify-center">
24-
<Button
25-
variant="outline"
26-
onClick={onLoadMore}
27-
className="w-full max-w-[200px]"
28-
>
24+
<Button variant="outline" onClick={onLoadMore} className="w-full max-w-[200px]">
2925
<Loader2 className="mr-2 h-4 w-4" />
3026
{t('containers.load_more_logs')}
3127
</Button>
3228
</div>
3329
</div>
3430
);
35-
}
31+
}

view/app/containers/[id]/components/OverviewTab.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ export function OverviewTab({ container }: OverviewTabProps) {
5858
</div>
5959
<div className="flex items-center justify-between">
6060
<span className="text-sm text-muted-foreground">{t('containers.memory')}</span>
61-
<span className="text-sm">{(container.host_config.memory / (1024 * 1024)).toFixed(2)} MB</span>
61+
<span className="text-sm">
62+
{(container.host_config.memory / (1024 * 1024)).toFixed(2)} MB
63+
</span>
6264
</div>
6365
</div>
6466
</CardContent>
@@ -97,4 +99,4 @@ export function OverviewTab({ container }: OverviewTabProps) {
9799
</Card>
98100
</div>
99101
);
100-
}
102+
}

view/app/containers/[id]/components/images.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
import { Button } from "@/components/ui/button";
2-
import {
3-
Card,
4-
CardContent,
5-
} from "@/components/ui/card";
6-
import { Skeleton } from "@/components/ui/skeleton";
1+
import { Button } from '@/components/ui/button';
2+
import { Card, CardContent } from '@/components/ui/card';
3+
import { Skeleton } from '@/components/ui/skeleton';
74
import {
85
Table,
96
TableBody,
107
TableCell,
118
TableHead,
129
TableHeader,
13-
TableRow,
14-
} from "@/components/ui/table";
15-
import { useTranslation } from "@/hooks/use-translation";
16-
import { formatBytes, formatDate } from "@/lib/utils";
17-
import { useGetImagesQuery } from "@/redux/services/container/imagesApi";
18-
import { Loader2 } from "lucide-react";
10+
TableRow
11+
} from '@/components/ui/table';
12+
import { useTranslation } from '@/hooks/use-translation';
13+
import { formatBytes, formatDate } from '@/lib/utils';
14+
import { useGetImagesQuery } from '@/redux/services/container/imagesApi';
15+
import { Loader2 } from 'lucide-react';
1916

2017
interface Image {
2118
id: string;
@@ -28,10 +25,10 @@ interface Image {
2825
labels: Record<string, string>;
2926
}
3027

31-
export function Images({ containerId, imagePrefix }: { containerId: string, imagePrefix: string }) {
28+
export function Images({ containerId, imagePrefix }: { containerId: string; imagePrefix: string }) {
3229
const { data: images = [], isLoading } = useGetImagesQuery({ containerId, imagePrefix });
3330
const { t } = useTranslation();
34-
31+
3532
if (isLoading) {
3633
return <ImagesSectionSkeleton />;
3734
}
@@ -42,10 +39,10 @@ export function Images({ containerId, imagePrefix }: { containerId: string, imag
4239
<Table>
4340
<TableHeader>
4441
<TableRow>
45-
<TableHead>{t("containers.images.id")}</TableHead>
46-
<TableHead>{t("containers.images.tags")}</TableHead>
47-
<TableHead>{t("containers.images.created")}</TableHead>
48-
<TableHead>{t("containers.images.size")}</TableHead>
42+
<TableHead>{t('containers.images.id')}</TableHead>
43+
<TableHead>{t('containers.images.tags')}</TableHead>
44+
<TableHead>{t('containers.images.created')}</TableHead>
45+
<TableHead>{t('containers.images.size')}</TableHead>
4946
</TableRow>
5047
</TableHeader>
5148
<TableBody>
@@ -59,9 +56,7 @@ export function Images({ containerId, imagePrefix }: { containerId: string, imag
5956
images?.map((image: Image) => (
6057
<TableRow key={image.id}>
6158
<TableCell className="font-mono">{image.id.slice(0, 12)}</TableCell>
62-
<TableCell>
63-
{image.repo_tags?.join(", ") || "<none>"}
64-
</TableCell>
59+
<TableCell>{image.repo_tags?.join(', ') || '<none>'}</TableCell>
6560
<TableCell>{formatDate(new Date(image.created * 1000))}</TableCell>
6661
<TableCell>{formatBytes(image.size)}</TableCell>
6762
</TableRow>
@@ -74,7 +69,6 @@ export function Images({ containerId, imagePrefix }: { containerId: string, imag
7469
);
7570
}
7671

77-
7872
export function ImagesSectionSkeleton() {
7973
return (
8074
<Card>
@@ -112,4 +106,4 @@ export function ImagesSectionSkeleton() {
112106
</CardContent>
113107
</Card>
114108
);
115-
}
109+
}

view/app/containers/[id]/page.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
'use client';
22

3-
import { Play, StopCircle, Trash2, Info, Terminal, HardDrive, RotateCw, ShipIcon, Layers } from 'lucide-react';
3+
import {
4+
Play,
5+
StopCircle,
6+
Trash2,
7+
Info,
8+
Terminal,
9+
HardDrive,
10+
RotateCw,
11+
ShipIcon,
12+
Layers
13+
} from 'lucide-react';
414
import { useTranslation } from '@/hooks/use-translation';
515
import { Button } from '@/components/ui/button';
616
import { toast } from 'sonner';
@@ -31,14 +41,14 @@ export default function ContainerDetailsPage() {
3141
const [stopContainer] = useStopContainerMutation();
3242
const [removeContainer] = useRemoveContainerMutation();
3343
const [logsTail, setLogsTail] = useState(100);
34-
const [allLogs, setAllLogs] = useState<string>("");
44+
const [allLogs, setAllLogs] = useState<string>('');
3545
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
3646

3747
const { data: logs, refetch: refetchLogs } = useGetContainerLogsQuery(
3848
{ containerId, tail: logsTail },
3949
{
4050
skip: !containerId,
41-
refetchOnMountOrArgChange: true,
51+
refetchOnMountOrArgChange: true
4252
}
4353
);
4454

@@ -85,7 +95,7 @@ export default function ContainerDetailsPage() {
8595
};
8696

8797
if (isLoading || !container) {
88-
return <ContainerDetailsLoading />
98+
return <ContainerDetailsLoading />;
8999
}
90100

91101
return (
@@ -160,25 +170,19 @@ export default function ContainerDetailsPage() {
160170
<OverviewTab container={container} />
161171
</TabsContent>
162172
<TabsContent value="logs" className="mt-4">
163-
<LogsTab
164-
container={container}
165-
logs={allLogs}
166-
onLoadMore={handleLoadMoreLogs}
167-
/>
173+
<LogsTab container={container} logs={allLogs} onLoadMore={handleLoadMoreLogs} />
168174
</TabsContent>
169175
<TabsContent value="details" className="mt-4">
170176
<DetailsTab container={container} />
171177
</TabsContent>
172178
<TabsContent value="images" className="mt-4">
173-
{
174-
container.image ? (
175-
<Images containerId={containerId} imagePrefix={container.image + "*"} />
176-
) : (
177-
<div className="flex items-center justify-center h-full">
178-
<p>{t('containers.images.none')}</p>
179-
</div>
180-
)
181-
}
179+
{container.image ? (
180+
<Images containerId={containerId} imagePrefix={container.image + '*'} />
181+
) : (
182+
<div className="flex items-center justify-center h-full">
183+
<p>{t('containers.images.none')}</p>
184+
</div>
185+
)}
182186
</TabsContent>
183187
</Tabs>
184188
</div>
@@ -196,4 +200,4 @@ export default function ContainerDetailsPage() {
196200
/>
197201
</div>
198202
);
199-
}
203+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useRouter } from 'next/navigation';
2+
import { useTranslation } from '@/hooks/use-translation';
3+
import React, { use, useState } from 'react';
4+
import { toast } from 'sonner';
5+
import { useAppSelector } from '@/redux/hooks';
6+
import {
7+
useRemoveContainerMutation,
8+
useStartContainerMutation,
9+
useStopContainerMutation
10+
} from '@/redux/services/container/containerApi';
11+
import { useGetContainersQuery } from '@/redux/services/container/containerApi';
12+
import { useFeatureFlags } from '@/hooks/features_provider';
13+
import { hasPermission } from '@/lib/permission';
14+
import { usePruneBuildCacheMutation } from '@/redux/services/container/imagesApi';
15+
import { usePruneImagesMutation } from '@/redux/services/container/imagesApi';
16+
17+
function useContainerList() {
18+
const { t } = useTranslation();
19+
const router = useRouter();
20+
const user = useAppSelector((state) => state.auth.user);
21+
const activeOrg = useAppSelector((state) => state.user.activeOrganization);
22+
const { data: containers = [], isLoading, refetch } = useGetContainersQuery();
23+
const [startContainer] = useStartContainerMutation();
24+
const [stopContainer] = useStopContainerMutation();
25+
const [removeContainer] = useRemoveContainerMutation();
26+
const [isRefreshing, setIsRefreshing] = useState(false);
27+
const [containerToDelete, setContainerToDelete] = useState<string | null>(null);
28+
const [showPruneImagesConfirm, setShowPruneImagesConfirm] = useState(false);
29+
const [showPruneBuildCacheConfirm, setShowPruneBuildCacheConfirm] = useState(false);
30+
const { isFeatureEnabled, isLoading: isFeatureFlagsLoading } = useFeatureFlags();
31+
32+
const canRead = hasPermission(user, 'container', 'read', activeOrg?.id);
33+
const canCreate = hasPermission(user, 'container', 'create', activeOrg?.id);
34+
const canUpdate = hasPermission(user, 'container', 'update', activeOrg?.id);
35+
const canDelete = hasPermission(user, 'container', 'delete', activeOrg?.id);
36+
37+
const [pruneImages] = usePruneImagesMutation();
38+
const [pruneBuildCache] = usePruneBuildCacheMutation();
39+
40+
const handleRefresh = async () => {
41+
setIsRefreshing(true);
42+
try {
43+
await refetch();
44+
} finally {
45+
setIsRefreshing(false);
46+
}
47+
};
48+
49+
const handleContainerAction = async (
50+
containerId: string,
51+
action: 'start' | 'stop' | 'remove'
52+
) => {
53+
try {
54+
switch (action) {
55+
case 'start':
56+
await startContainer(containerId).unwrap();
57+
toast.success(t(`containers.${action}_success`));
58+
break;
59+
case 'stop':
60+
await stopContainer(containerId).unwrap();
61+
toast.success(t(`containers.${action}_success`));
62+
break;
63+
case 'remove':
64+
setContainerToDelete(containerId);
65+
break;
66+
}
67+
} catch (error) {
68+
toast.error(t(`containers.${action}_error`));
69+
}
70+
};
71+
72+
const handleDeleteConfirm = async () => {
73+
if (!containerToDelete) return;
74+
try {
75+
await removeContainer(containerToDelete).unwrap();
76+
toast.success(t('containers.remove_success'));
77+
setContainerToDelete(null);
78+
} catch (error) {
79+
toast.error(t('containers.remove_error'));
80+
}
81+
};
82+
83+
const handlePruneImages = async () => {
84+
try {
85+
await pruneImages({
86+
dangling: true
87+
}).unwrap();
88+
toast.success(t('containers.prune_images_success'));
89+
} catch (error) {
90+
toast.error(t('containers.prune_images_error'));
91+
}
92+
};
93+
94+
const handlePruneBuildCache = async () => {
95+
try {
96+
await pruneBuildCache({
97+
all: true
98+
}).unwrap();
99+
toast.success(t('containers.prune_build_cache_success'));
100+
} catch (error) {
101+
toast.error(t('containers.prune_build_cache_error'));
102+
}
103+
};
104+
105+
const getGradientFromName = (name: string) => {
106+
const colors = [
107+
'from-blue-500/20 to-purple-500/20',
108+
'from-green-500/20 to-teal-500/20',
109+
'from-yellow-500/20 to-orange-500/20',
110+
'from-red-500/20 to-pink-500/20',
111+
'from-indigo-500/20 to-violet-500/20',
112+
'from-emerald-500/20 to-cyan-500/20'
113+
];
114+
const index = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
115+
return colors[index];
116+
};
117+
118+
return {
119+
containers,
120+
isLoading,
121+
refetch,
122+
handleRefresh,
123+
handleContainerAction,
124+
handleDeleteConfirm,
125+
handlePruneImages,
126+
handlePruneBuildCache,
127+
showPruneImagesConfirm,
128+
showPruneBuildCacheConfirm,
129+
setShowPruneImagesConfirm,
130+
setShowPruneBuildCacheConfirm,
131+
canRead,
132+
canCreate,
133+
canUpdate,
134+
canDelete,
135+
isFeatureFlagsLoading,
136+
isRefreshing,
137+
isFeatureEnabled,
138+
t,
139+
router,
140+
containerToDelete,
141+
setContainerToDelete,
142+
getGradientFromName
143+
};
144+
}
145+
146+
export default useContainerList;

0 commit comments

Comments
 (0)