Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: list and create backup #852

Merged
merged 11 commits into from
Nov 28, 2024
38 changes: 36 additions & 2 deletions src/frontend/src/lib/api/ic.api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type {
canister_log_record,
canister_settings,
log_visibility
log_visibility,
snapshot,
snapshot_id
} from '$declarations/ic/ic.did';
import { getICActor } from '$lib/api/actors/actor.ic.api';
import { getAgent } from '$lib/api/agent/agent.api';
import type { CanisterInfo, CanisterLogVisibility, CanisterStatus } from '$lib/types/canister';
import type { Snapshots } from '$lib/types/snapshot';
import {
CanisterStatus as AgentCanisterStatus,
AnonymousIdentity,
type Identity
} from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import { nonNullish } from '@dfinity/utils';
import { nonNullish, toNullable } from '@dfinity/utils';

const toStatus = (
status: { stopped: null } | { stopping: null } | { running: null }
Expand Down Expand Up @@ -121,6 +124,37 @@ export const canisterLogs = async ({
return canister_log_records;
};

export const canisterSnapshots = async ({
canisterId,
identity
}: {
canisterId: Principal;
identity: Identity;
}): Promise<Snapshots> => {
const { list_canister_snapshots } = await getICActor({ identity });

return await list_canister_snapshots({
canister_id: canisterId
});
};

export const createSnapshot = async ({
canisterId,
snapshotId,
identity
}: {
canisterId: Principal;
snapshotId?: snapshot_id;
identity: Identity;
}): Promise<snapshot> => {
const { take_canister_snapshot } = await getICActor({ identity });

return await take_canister_snapshot({
canister_id: canisterId,
replace_snapshot: toNullable(snapshotId)
});
};

export const canisterUpdateSettings = async ({
canisterId,
identity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { Principal } from '@dfinity/principal';
import AnalyticsControllers from '$lib/components/analytics/AnalyticsControllers.svelte';
import CanisterSettings from '$lib/components/canister/CanisterSettings.svelte';
import CanisterSnapshots from '$lib/components/canister/CanisterSnapshots.svelte';
import { i18n } from '$lib/stores/i18n.store';

interface Props {
Expand All @@ -21,6 +22,12 @@

<AnalyticsControllers {orbiterId} />

<CanisterSnapshots
canisterId={orbiterId}
segment="orbiter"
segmentLabel={$i18n.analytics.orbiter}
/>

<style lang="scss">
div {
margin: var(--padding-8x) 0 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

const openModal = () => {
if (isNullish(settings)) {
toasts.error({ text: $i18n.errors.canister_settings_no_loaded });
toasts.error({ text: $i18n.errors.canister_settings_not_loaded });
return;
}

Expand Down
132 changes: 132 additions & 0 deletions src/frontend/src/lib/components/canister/CanisterSnapshots.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script lang="ts">
import { encodeSnapshotId } from '@dfinity/ic-management';
import type { Principal } from '@dfinity/principal';
import { isNullish, nonNullish } from '@dfinity/utils';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import Identifier from '$lib/components/ui/Identifier.svelte';
import SkeletonText from '$lib/components/ui/SkeletonText.svelte';
import { loadSnapshots } from '$lib/services/snapshots.services';
import { authStore } from '$lib/stores/auth.store';
import { i18n } from '$lib/stores/i18n.store';
import { snapshotStore } from '$lib/stores/snapshot.store';
import { toasts } from '$lib/stores/toasts.store';
import type { Segment } from '$lib/types/canister';
import type { Snapshots } from '$lib/types/snapshot';
import type { Option } from '$lib/types/utils';
import { formatToDate } from '$lib/utils/date.utils';
import { emit } from '$lib/utils/events.utils';
import { i18nFormat } from '$lib/utils/i18n.utils';
import { formatBytes } from '$lib/utils/number.utils';

interface Props {
canisterId: Principal;
segment: Segment;
segmentLabel: string;
}

let { canisterId, segment, segmentLabel }: Props = $props();

onMount(() => {
loadSnapshots({
canisterId,
identity: $authStore.identity
});
});

let snapshots: Option<Snapshots> = $derived($snapshotStore?.[canisterId.toText()]);

const openModal = () => {
if (isNullish(snapshots)) {
toasts.error({ text: $i18n.errors.snapshot_not_loaded });
return;
}

emit({
message: 'junoModal',
detail: {
type: 'create_snapshot',
detail: {
segment: {
canisterId: canisterId.toText(),
segment,
label: segmentLabel
}
}
}
});
};
</script>

<div class="table-container">
<table>
<thead>
<tr>
<th class="backup"> {$i18n.canisters.backup} </th>
<th class="size"> {$i18n.canisters.size} </th>
<th> {$i18n.canisters.timestamp} </th>
</tr>
</thead>

<tbody>
{#if nonNullish(snapshots)}
{#each snapshots as snapshot}
<tr>
<td><Identifier small={false} identifier={encodeSnapshotId(snapshot.id)} /></td>
<td>{formatBytes(Number(snapshot.total_size))}</td>
<td>{formatToDate(snapshot.taken_at_timestamp)}</td>
</tr>
{/each}

{#if snapshots.length === 0}
<tr in:fade
><td colspan="3"
>{i18nFormat($i18n.canisters.no_backup, [
{
placeholder: '{0}',
value: segmentLabel
}
])}</td
></tr
>
{/if}
{:else}
<tr
><td colspan="3">
<div class="skeleton">
&ZeroWidthSpace;<SkeletonText />
</div>
</td></tr
>
{/if}
</tbody>
</table>
</div>

<button onclick={openModal}>{$i18n.core.create}</button>

<style lang="scss">
@use '../../styles/mixins/media';

.backup {
@include media.min-width(medium) {
width: 25%;
}
}

.size {
@include media.min-width(medium) {
width: 25%;
}
}

.table-container {
margin: var(--padding-8x) 0 var(--padding-2x);
}

.skeleton {
display: flex;
max-width: 200px;
--skeleton-text-padding: var(--padding-0_25x) 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import { isNullish } from '@dfinity/utils';
import { untrack } from 'svelte';
import WizardProgressSteps from '$lib/components/ui/WizardProgressSteps.svelte';
import { i18n } from '$lib/stores/i18n.store';
import type { ProgressStep } from '$lib/types/progress-step';
import { type CreateSnapshotProgress, CreateSnapshotProgressStep } from '$lib/types/snapshot';
import { i18nFormat } from '$lib/utils/i18n.utils';
import { mapProgressState } from '$lib/utils/progress.utils';

interface Props {
segment: 'satellite' | 'mission_control' | 'orbiter';
progress: CreateSnapshotProgress | undefined;
}

let { progress, segment }: Props = $props();

interface Steps {
preparing: ProgressStep;
stopping: ProgressStep;
creating: ProgressStep;
restarting: ProgressStep;
}

let steps: Steps = $state({
preparing: {
state: 'in_progress',
step: 'preparing',
text: $i18n.canisters.backup_preparing
},
stopping: {
state: 'next',
step: 'stopping',
text: i18nFormat($i18n.canisters.backup_stopping, [
{
placeholder: '{0}',
value: segment.replace('_', ' ')
}
])
},
creating: {
state: 'next',
step: 'creating',
text: $i18n.canisters.creating_backup
},
restarting: {
state: 'next',
step: 'restarting',
text: i18nFormat($i18n.canisters.upgrade_restarting, [
{
placeholder: '{0}',
value: segment.replace('_', ' ')
}
])
}
});

let displaySteps = $derived(Object.values(steps) as [ProgressStep, ...ProgressStep[]]);

$effect(() => {
progress;

untrack(() => {
const { preparing, stopping, creating, restarting } = steps;

steps = {
preparing: {
...preparing,
state: isNullish(progress) ? 'in_progress' : 'completed'
},
stopping: {
...stopping,
state:
progress?.step === CreateSnapshotProgressStep.StoppingCanister
? mapProgressState(progress?.state)
: stopping.state
},
creating: {
...creating,
state:
progress?.step === CreateSnapshotProgressStep.CreatingSnapshot
? mapProgressState(progress?.state)
: creating.state
},
restarting: {
...restarting,
state:
progress?.step === CreateSnapshotProgressStep.RestartingCanister
? mapProgressState(progress?.state)
: restarting.state
}
};
});
});
</script>

<WizardProgressSteps steps={displaySteps}>
{$i18n.core.hold_tight}
</WizardProgressSteps>
2 changes: 1 addition & 1 deletion src/frontend/src/lib/components/docs/DocUpload.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
{/if}

{#snippet confirm()}
{mode === 'replace' ? $i18n.document.replace : $i18n.document.create}
{mode === 'replace' ? $i18n.core.replace : $i18n.core.create}
{/snippet}
</DataUpload>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import type { Principal } from '@dfinity/principal';
import CanisterSettings from '$lib/components/canister/CanisterSettings.svelte';
import CanisterSnapshots from '$lib/components/canister/CanisterSnapshots.svelte';
import MissionControlControllers from '$lib/components/mission-control/MissionControlControllers.svelte';
import { i18n } from '$lib/stores/i18n.store';

Expand All @@ -18,3 +19,9 @@
/>

<MissionControlControllers {missionControlId} />

<CanisterSnapshots
canisterId={missionControlId}
segment="mission_control"
segmentLabel={$i18n.mission_control.title}
/>
Loading