AppsCleanup.tsx raw
1 import { SkipForwardIcon, Trash2Icon, TriangleAlertIcon } from "lucide-react";
2 import React from "react";
3 import AppHeader from "src/components/AppHeader";
4 import AppCard from "src/components/connections/AppCard";
5 import Loading from "src/components/Loading";
6 import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
7 import { Button } from "src/components/ui/button";
8 import {
9 Card,
10 CardDescription,
11 CardFooter,
12 CardHeader,
13 CardTitle,
14 } from "src/components/ui/card";
15 import { LinkButton } from "src/components/ui/custom/link-button";
16 import { Progress } from "src/components/ui/progress";
17 import { useDeleteApp } from "src/hooks/useDeleteApp";
18 import { useUnusedApps } from "src/hooks/useUnusedApps";
19 import { App } from "src/types";
20
21 export function AppsCleanup() {
22 const unusedApps = useUnusedApps(100_000); // assume never more than 100k apps
23 const [appIndex, setAppIndex] = React.useState<number>();
24 const [skippedCount, setSkippedCount] = React.useState<number>(0);
25 const [deletedCount, setDeletedCount] = React.useState<number>(0);
26 const [appsToReview, setAppsToReview] = React.useState<App[]>();
27 React.useEffect(() => {
28 if (!unusedApps) {
29 return;
30 }
31 // only allow initializing once
32 if (appIndex === undefined) {
33 setAppIndex(0);
34
35 const _appsToReview = [...unusedApps];
36 _appsToReview.sort((a, b) => {
37 return (
38 (a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0) -
39 (b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0)
40 );
41 });
42 setAppsToReview(_appsToReview);
43 }
44 }, [appIndex, unusedApps]);
45
46 if (!appsToReview || appIndex === undefined) {
47 return <Loading />;
48 }
49
50 const currentApp = appsToReview[appIndex];
51
52 return (
53 <>
54 <AppHeader
55 pageTitle="Cleanup Unused Apps"
56 title="Cleanup Unused Apps"
57 description="Review apps that haven't been used for 2 months or longer"
58 />
59 {currentApp && (
60 <Alert variant="destructive">
61 <AlertTitle className="flex gap-2">
62 <TriangleAlertIcon className="h-4 w-4" />
63 Warning
64 </AlertTitle>
65 <AlertDescription>
66 Review the app carefully before deleting it, deleted apps cannot be
67 recovered.
68 </AlertDescription>
69 </Alert>
70 )}
71 <div className="grid grid-cols-1 lg:grid-cols-5 gap-3">
72 <div className="lg:col-span-3 flex flex-col gap-3">
73 {currentApp && (
74 <>
75 <div className="w-full h-full">
76 <AppCard
77 app={currentApp}
78 actions={
79 <>
80 <Button
81 onClick={() => {
82 setAppIndex(appIndex + 1);
83 setSkippedCount((current) => current + 1);
84 }}
85 >
86 <SkipForwardIcon />
87 Skip
88 </Button>
89 <DeleteAppButton
90 app={currentApp}
91 onDelete={() => {
92 setAppIndex(appIndex + 1);
93 setDeletedCount((current) => current + 1);
94 }}
95 />
96 </>
97 }
98 readonly
99 />
100 </div>
101 </>
102 )}
103 {!currentApp && (
104 <>
105 <div>No more unused apps to review.</div>
106 <div>
107 <LinkButton to="/apps?tab=connected-apps">
108 Back to overview
109 </LinkButton>
110 </div>
111 </>
112 )}
113 </div>
114 <div className="lg:col-span-2">
115 <Card className="w-full">
116 <CardHeader>
117 <CardTitle>Progress</CardTitle>
118 <CardDescription>
119 {appIndex} of {appsToReview.length} unused apps reviewed (
120 {skippedCount} skipped, {deletedCount} deleted)
121 </CardDescription>
122 </CardHeader>
123 <CardFooter>
124 <Progress value={(appIndex / appsToReview.length) * 100} />
125 </CardFooter>
126 </Card>
127 </div>
128 </div>
129 </>
130 );
131 }
132
133 function DeleteAppButton({
134 app,
135 onDelete,
136 }: {
137 app: App;
138 onDelete: () => void;
139 }) {
140 const { deleteApp } = useDeleteApp(app);
141 return (
142 <Button
143 variant="destructive"
144 onClick={() => {
145 deleteApp();
146 onDelete();
147 }}
148 >
149 <Trash2Icon />
150 Delete
151 </Button>
152 );
153 }
154