Skip to content

Commit 0ad837c

Browse files
authored
fix: inconsistient behavior with pull and redeploy button via project table (#1953)
1 parent 930bc7f commit 0ad837c

4 files changed

Lines changed: 308 additions & 235 deletions

File tree

cli/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/fatih/color v1.18.0
1111
github.com/getarcaneapp/arcane/types v1.15.3
1212
github.com/go-viper/mapstructure/v2 v2.5.0
13+
github.com/mattn/go-runewidth v0.0.20
1314
github.com/sirupsen/logrus v1.9.4
1415
github.com/spf13/cobra v1.10.2
1516
github.com/spf13/pflag v1.0.10
@@ -37,7 +38,6 @@ require (
3738
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
3839
github.com/mattn/go-colorable v0.1.14 // indirect
3940
github.com/mattn/go-isatty v0.0.20 // indirect
40-
github.com/mattn/go-runewidth v0.0.20 // indirect
4141
github.com/mattn/go-shellwords v1.0.12 // indirect
4242
github.com/moby/docker-image-spec v1.3.1 // indirect
4343
github.com/muesli/cancelreader v0.2.2 // indirect
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { openConfirmDialog } from '$lib/components/confirm-dialog';
2+
import { m } from '$lib/paraglide/messages';
3+
import { deployOptionsStore } from '$lib/stores/deploy-options.store.svelte';
4+
import { gitOpsSyncService } from '$lib/services/gitops-sync-service';
5+
import { projectService } from '$lib/services/project-service';
6+
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
7+
import { handleApiResultWithCallbacks } from '$lib/utils/api.util';
8+
import { tryCatch } from '$lib/utils/try-catch';
9+
import { toast } from 'svelte-sonner';
10+
import type { ActionStatus } from './projects-table.helpers';
11+
12+
type BulkLoadingState = {
13+
up: boolean;
14+
down: boolean;
15+
redeploy: boolean;
16+
};
17+
18+
type ActionDeps = {
19+
getRequestOptions: () => SearchPaginationSortRequest;
20+
refreshProjects: (options?: SearchPaginationSortRequest) => Promise<void>;
21+
setSelectedIds: (next: string[]) => void;
22+
actionStatus: Record<string, ActionStatus>;
23+
isBulkLoading: BulkLoadingState;
24+
getEnvId: () => string | undefined;
25+
};
26+
27+
type ProjectActionKind = 'start' | 'stop' | 'restart' | 'redeploy';
28+
29+
type ProjectActionConfig = {
30+
status: ActionStatus;
31+
run: (id: string) => Promise<unknown>;
32+
success: () => string;
33+
failure: () => string;
34+
};
35+
36+
type BulkActionConfig = {
37+
title: (count: number) => string;
38+
message: (count: number) => string;
39+
label: string;
40+
loadingKey: keyof BulkLoadingState;
41+
run: (id: string) => Promise<unknown>;
42+
success: (count: number) => string;
43+
partial: (success: number, total: number, failed: number) => string;
44+
failure: () => string;
45+
destructive?: boolean;
46+
};
47+
48+
type DestroyConfirmResult = {
49+
checkboxes?: {
50+
volumes?: boolean;
51+
files?: boolean;
52+
};
53+
volumes?: boolean;
54+
files?: boolean;
55+
};
56+
57+
type ProjectActions = {
58+
performProjectAction: (action: ProjectActionKind, id: string) => Promise<void>;
59+
handleDestroyProject: (id: string) => Promise<void>;
60+
handleSyncFromGit: (projectId: string, gitOpsSyncId: string) => Promise<void>;
61+
handleBulkUp: (ids: string[]) => Promise<void>;
62+
handleBulkDown: (ids: string[]) => Promise<void>;
63+
handleBulkRedeploy: (ids: string[]) => Promise<void>;
64+
};
65+
66+
const projectActionConfigs: Record<ProjectActionKind, ProjectActionConfig> = {
67+
start: {
68+
status: 'starting',
69+
run: (id) => projectService.deployProject(id, deployOptionsStore.getRequestOptions()),
70+
success: () => m.compose_start_success(),
71+
failure: () => m.compose_start_failed()
72+
},
73+
stop: {
74+
status: 'stopping',
75+
run: (id) => projectService.downProject(id),
76+
success: () => m.compose_stop_success(),
77+
failure: () => m.compose_stop_failed()
78+
},
79+
restart: {
80+
status: 'restarting',
81+
run: (id) => projectService.restartProject(id),
82+
success: () => m.compose_restart_success(),
83+
failure: () => m.compose_restart_failed()
84+
},
85+
redeploy: {
86+
status: 'redeploying',
87+
run: (id) => projectService.redeployProject(id),
88+
success: () => m.compose_pull_success(),
89+
failure: () => m.compose_pull_failed()
90+
}
91+
};
92+
93+
export function createProjectActions({
94+
getRequestOptions,
95+
refreshProjects,
96+
setSelectedIds,
97+
actionStatus,
98+
isBulkLoading,
99+
getEnvId
100+
}: ActionDeps): ProjectActions {
101+
async function performProjectAction(action: ProjectActionKind, id: string): Promise<void> {
102+
const config = projectActionConfigs[action];
103+
actionStatus[id] = config.status;
104+
105+
try {
106+
await handleApiResultWithCallbacks({
107+
result: await tryCatch(config.run(id)),
108+
message: config.failure(),
109+
setLoadingState: (value) => {
110+
actionStatus[id] = value ? config.status : '';
111+
},
112+
onSuccess: async () => {
113+
toast.success(config.success());
114+
await refreshProjects();
115+
}
116+
});
117+
} catch (error) {
118+
toast.error(m.common_action_failed());
119+
actionStatus[id] = '';
120+
}
121+
}
122+
123+
async function handleDestroyProject(id: string): Promise<void> {
124+
openConfirmDialog({
125+
title: m.common_confirm_removal_title(),
126+
message: m.compose_confirm_removal_message(),
127+
checkboxes: [
128+
{
129+
id: 'volumes',
130+
label: m.confirm_remove_volumes_warning(),
131+
initialState: false
132+
},
133+
{
134+
id: 'files',
135+
label: m.confirm_remove_project_files(),
136+
initialState: false
137+
}
138+
],
139+
confirm: {
140+
label: m.compose_destroy(),
141+
destructive: true,
142+
action: async (result: DestroyConfirmResult) => {
143+
const removeVolumes = !!(result?.checkboxes?.volumes ?? result?.volumes);
144+
const removeFiles = !!(result?.checkboxes?.files ?? result?.files);
145+
actionStatus[id] = 'destroying';
146+
147+
await handleApiResultWithCallbacks({
148+
result: await tryCatch(projectService.destroyProject(id, removeVolumes, removeFiles)),
149+
message: m.compose_destroy_failed(),
150+
setLoadingState: (value) => {
151+
actionStatus[id] = value ? 'destroying' : '';
152+
},
153+
onSuccess: async () => {
154+
toast.success(m.compose_destroy_success());
155+
await refreshProjects();
156+
}
157+
});
158+
}
159+
}
160+
});
161+
}
162+
163+
async function handleSyncFromGit(projectId: string, gitOpsSyncId: string): Promise<void> {
164+
const envId = getEnvId();
165+
if (!envId) return;
166+
167+
actionStatus[projectId] = 'syncing';
168+
const result = await tryCatch(gitOpsSyncService.performSync(envId, gitOpsSyncId));
169+
170+
await handleApiResultWithCallbacks({
171+
result,
172+
message: m.git_sync_failed(),
173+
setLoadingState: (value) => {
174+
actionStatus[projectId] = value ? 'syncing' : '';
175+
},
176+
onSuccess: async () => {
177+
toast.success(m.git_sync_success());
178+
await refreshProjects();
179+
}
180+
});
181+
}
182+
183+
async function runBulkAction(ids: string[], config: BulkActionConfig): Promise<void> {
184+
if (!ids || ids.length === 0) return;
185+
186+
openConfirmDialog({
187+
title: config.title(ids.length),
188+
message: config.message(ids.length),
189+
confirm: {
190+
label: config.label,
191+
destructive: config.destructive ?? false,
192+
action: async () => {
193+
isBulkLoading[config.loadingKey] = true;
194+
195+
try {
196+
const results = await Promise.allSettled(ids.map((id) => config.run(id)));
197+
198+
const successCount = results.filter((result) => result.status === 'fulfilled').length;
199+
const failureCount = results.length - successCount;
200+
201+
if (successCount === ids.length) {
202+
toast.success(config.success(successCount));
203+
} else if (successCount > 0) {
204+
toast.warning(config.partial(successCount, ids.length, failureCount));
205+
} else {
206+
toast.error(config.failure());
207+
}
208+
209+
await refreshProjects(getRequestOptions());
210+
setSelectedIds([]);
211+
} finally {
212+
isBulkLoading[config.loadingKey] = false;
213+
}
214+
}
215+
}
216+
});
217+
}
218+
219+
async function handleBulkUp(ids: string[]): Promise<void> {
220+
await runBulkAction(ids, {
221+
title: (count) => m.projects_bulk_up_confirm_title({ count }),
222+
message: (count) => m.projects_bulk_up_confirm_message({ count }),
223+
label: m.common_up(),
224+
loadingKey: 'up',
225+
run: (id) => projectService.deployProject(id, deployOptionsStore.getRequestOptions()),
226+
success: (count) => m.projects_bulk_up_success({ count }),
227+
partial: (success, total, failed) => m.projects_bulk_up_partial({ success, total, failed }),
228+
failure: () => m.compose_start_failed()
229+
});
230+
}
231+
232+
async function handleBulkDown(ids: string[]): Promise<void> {
233+
await runBulkAction(ids, {
234+
title: (count) => m.projects_bulk_down_confirm_title({ count }),
235+
message: (count) => m.projects_bulk_down_confirm_message({ count }),
236+
label: m.common_down(),
237+
loadingKey: 'down',
238+
run: (id) => projectService.downProject(id),
239+
success: (count) => m.projects_bulk_down_success({ count }),
240+
partial: (success, total, failed) => m.projects_bulk_down_partial({ success, total, failed }),
241+
failure: () => m.compose_stop_failed()
242+
});
243+
}
244+
245+
async function handleBulkRedeploy(ids: string[]): Promise<void> {
246+
await runBulkAction(ids, {
247+
title: (count) => m.projects_bulk_redeploy_confirm_title({ count }),
248+
message: (count) => m.projects_bulk_redeploy_confirm_message({ count }),
249+
label: m.compose_pull_redeploy(),
250+
loadingKey: 'redeploy',
251+
run: (id) => projectService.redeployProject(id),
252+
success: (count) => m.projects_bulk_redeploy_success({ count }),
253+
partial: (success, total, failed) => m.projects_bulk_redeploy_partial({ success, total, failed }),
254+
failure: () => m.compose_pull_failed()
255+
});
256+
}
257+
258+
return {
259+
performProjectAction,
260+
handleDestroyProject,
261+
handleSyncFromGit,
262+
handleBulkUp,
263+
handleBulkDown,
264+
handleBulkRedeploy
265+
};
266+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ActionStatus = 'starting' | 'stopping' | 'restarting' | 'redeploying' | 'destroying' | 'syncing' | '';

0 commit comments

Comments
 (0)