fix(environments): allow deleting disconnected environments
Signed-off-by: Solaris-star <[email protected]>
This commit is contained in:
1 parent
0b6305ba2c
commit
f24f42116e
3 files changed
+113
-23
No files matched your search
@@ -0,0 +1,42 @@
|
||||
export interface EnvironmentDeleteCounts {
|
||||
stackCount: number;
|
||||
gitStackCount: number;
|
||||
unknown: boolean;
|
||||
}
|
||||
|
||||
type FetchEnvironmentDeleteCounts = (input: string) => Promise<Response>;
|
||||
|
||||
async function readArray(response: Response | null): Promise<{ count: number; unknown: boolean }> {
|
||||
if (!response?.ok) {
|
||||
return { count: 0, unknown: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const value: unknown = await response.json();
|
||||
return Array.isArray(value)
|
||||
? { count: value.length, unknown: false }
|
||||
: { count: 0, unknown: true };
|
||||
} catch {
|
||||
return { count: 0, unknown: true };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchEnvironmentDeleteCounts(
|
||||
environmentId: number,
|
||||
fetcher: FetchEnvironmentDeleteCounts = fetch
|
||||
): Promise<EnvironmentDeleteCounts> {
|
||||
const [stacksResponse, gitStacksResponse] = await Promise.all([
|
||||
fetcher(`/api/stacks?env=${environmentId}`).catch(() => null),
|
||||
fetcher(`/api/git/stacks?env=${environmentId}`).catch(() => null)
|
||||
]);
|
||||
const [stacks, gitStacks] = await Promise.all([
|
||||
readArray(stacksResponse),
|
||||
readArray(gitStacksResponse)
|
||||
]);
|
||||
|
||||
return {
|
||||
stackCount: stacks.count,
|
||||
gitStackCount: gitStacks.count,
|
||||
unknown: stacks.unknown || gitStacks.unknown
|
||||
};
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
import EnvironmentModal from './EnvironmentModal.svelte';
|
||||
import { environments as environmentsStore } from '$lib/stores/environment';
|
||||
import { dashboardData } from '$lib/stores/dashboard';
|
||||
import { fetchEnvironmentDeleteCounts } from '$lib/utils/environment-delete';
|
||||
|
||||
interface Props {
|
||||
editEnvId?: string | null;
|
||||
@@ -110,6 +111,7 @@
|
||||
let deleteStackCount = $state(0);
|
||||
let deleteGitStackCount = $state(0);
|
||||
let deleteCountsUnknown = $state(false);
|
||||
let deleteCountsLoading = $state(false);
|
||||
|
||||
// Track which environments have scanner enabled (for shield indicator)
|
||||
let envScannerStatus = $state<{ [id: number]: boolean }>({});
|
||||
@@ -208,40 +210,38 @@
|
||||
const env = environments.find(e => e.id === id);
|
||||
if (!env) return;
|
||||
|
||||
const [stacksRes, gitRes] = await Promise.all([
|
||||
fetch(`/api/stacks?env=${id}`).catch(() => null),
|
||||
fetch(`/api/git/stacks?env=${id}`).catch(() => null)
|
||||
]);
|
||||
|
||||
let unknown = false;
|
||||
let stacks: unknown[] = [];
|
||||
let gitStacks: unknown[] = [];
|
||||
if (stacksRes?.ok) {
|
||||
try { stacks = await stacksRes.json(); } catch { unknown = true; }
|
||||
} else {
|
||||
unknown = true;
|
||||
}
|
||||
if (gitRes?.ok) {
|
||||
try { gitStacks = await gitRes.json(); } catch { unknown = true; }
|
||||
} else {
|
||||
unknown = true;
|
||||
}
|
||||
deleteStackCount = Array.isArray(stacks) ? stacks.length : 0;
|
||||
deleteGitStackCount = Array.isArray(gitStacks) ? gitStacks.length : 0;
|
||||
deleteCountsUnknown = unknown;
|
||||
deleteEnvTarget = env;
|
||||
deleteStackCount = 0;
|
||||
deleteGitStackCount = 0;
|
||||
deleteCountsUnknown = true;
|
||||
deleteCountsLoading = true;
|
||||
showDeleteConfirm = true;
|
||||
|
||||
const counts = await fetchEnvironmentDeleteCounts(id);
|
||||
if (deleteEnvTarget?.id !== id || !showDeleteConfirm) return;
|
||||
|
||||
deleteStackCount = counts.stackCount;
|
||||
deleteGitStackCount = counts.gitStackCount;
|
||||
deleteCountsUnknown = counts.unknown;
|
||||
deleteCountsLoading = false;
|
||||
}
|
||||
|
||||
async function confirmAndDelete() {
|
||||
const target = deleteEnvTarget;
|
||||
showDeleteConfirm = false;
|
||||
deleteEnvTarget = null;
|
||||
deleteCountsLoading = false;
|
||||
if (target) {
|
||||
await deleteEnvironment(target.id);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDelete() {
|
||||
showDeleteConfirm = false;
|
||||
deleteEnvTarget = null;
|
||||
deleteCountsLoading = false;
|
||||
}
|
||||
|
||||
async function testConnection(id: number) {
|
||||
testingEnvs.add(id);
|
||||
testingEnvs = new Set(testingEnvs);
|
||||
@@ -718,7 +718,12 @@
|
||||
<code class="whitespace-nowrap">$DATA_DIR/git-repos/{deleteEnvTarget.name}/</code>
|
||||
</div>
|
||||
</div>
|
||||
{#if deleteCountsUnknown}
|
||||
{#if deleteCountsLoading}
|
||||
<p class="flex items-center gap-2">
|
||||
<RefreshCw class="w-3.5 h-3.5 animate-spin" />
|
||||
Checking tracked stacks…
|
||||
</p>
|
||||
{:else if deleteCountsUnknown}
|
||||
<p>
|
||||
Couldn't list the stacks on this environment — proceed only
|
||||
if you're sure what's deployed here.
|
||||
@@ -748,7 +753,7 @@
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<Button variant="outline" onclick={() => (showDeleteConfirm = false)}>
|
||||
<Button variant="outline" onclick={cancelDelete}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onclick={confirmAndDelete}>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import { fetchEnvironmentDeleteCounts } from '../src/lib/utils/environment-delete';
|
||||
|
||||
describe('fetchEnvironmentDeleteCounts', () => {
|
||||
it('returns stack counts when both endpoints respond', async () => {
|
||||
const result = await fetchEnvironmentDeleteCounts(42, async (url) => {
|
||||
return new Response(JSON.stringify(url.includes('/api/git/') ? [{}, {}] : [{}]));
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
stackCount: 1,
|
||||
gitStackCount: 2,
|
||||
unknown: false
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when a disconnected endpoint rejects', async () => {
|
||||
const result = await fetchEnvironmentDeleteCounts(42, async (url) => {
|
||||
if (url.startsWith('/api/stacks')) {
|
||||
throw new Error('connection timed out');
|
||||
}
|
||||
return new Response(JSON.stringify([]));
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
stackCount: 0,
|
||||
gitStackCount: 0,
|
||||
unknown: true
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when an endpoint returns an invalid payload', async () => {
|
||||
const result = await fetchEnvironmentDeleteCounts(42, async (url) => {
|
||||
return url.includes('/api/git/')
|
||||
? new Response('{invalid json')
|
||||
: new Response(JSON.stringify([]));
|
||||
});
|
||||
|
||||
assert.equal(result.unknown, true);
|
||||
assert.equal(result.gitStackCount, 0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user