dashboard/shell/components/PromptRestore.vue

307 lines
8.3 KiB
Vue

<script>
import { mapState, mapGetters } from 'vuex';
import AsyncButton from '@shell/components/AsyncButton';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import Date from '@shell/components/formatter/Date.vue';
import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { CAPI, SNAPSHOT } from '@shell/config/types';
import { set } from '@shell/utils/object';
import ChildHook, { BEFORE_SAVE_HOOKS } from '@shell/mixins/child-hook';
import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
import { escapeHtml } from '@shell/utils/string';
import day from 'dayjs';
import { sortBy } from '@shell/utils/sort';
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
import AppModal from '@shell/components/AppModal.vue';
export default {
components: {
Card,
AsyncButton,
Banner,
Date,
LabeledSelect,
RadioGroup,
AppModal,
},
name: 'PromptRestore',
mixins: [
ChildHook,
],
data() {
return {
errors: [],
labels: {},
restoreMode: 'all',
loaded: false,
allSnapshots: {},
sortedSnapshots: [],
selectedSnapshot: null,
};
},
computed: {
// toRestore can be a provisioning.cattle.io.cluster or a rke.cattle.io.etcdsnapshot resource
...mapState('action-menu', ['showPromptRestore', 'toRestore']),
...mapGetters({ t: 'i18n/t' }),
// Was the dialog opened to restore a specific snapshot, or opened on a cluster to choose
isCluster() {
const isSnapshot = this.toRestore[0]?.type.toLowerCase() === SNAPSHOT;
return !isSnapshot;
},
snapshot() {
return !this.isCluster ? this.toRestore[0] : this.allSnapshots[this.selectedSnapshot];
},
hasSnapshot() {
return !!this.snapshot;
},
isRke2() {
return !!this.snapshot?.rke2;
},
clusterSnapshots() {
if (this.sortedSnapshots) {
return this.sortedSnapshots.map((snapshot) => ({ label: this.snapshotLabel(snapshot), value: snapshot.name }));
} else {
return [];
}
},
restoreModeOptions() {
return ['none', 'kubernetesVersion', 'all'];
}
},
watch: {
async showPromptRestore(show) {
if (show) {
this.loaded = true;
await this.fetchSnapshots();
this.selectDefaultSnapshot();
} else {
this.loaded = false;
}
}
},
methods: {
close() {
this.errors = [];
this.labels = {};
this.$store.commit('action-menu/togglePromptRestore');
this.selectedSnapshot = null;
},
// If the user needs to choose a snapshot, fetch all snapshots for the cluster
async fetchSnapshots() {
if (!this.isCluster) {
return;
}
const cluster = this.toRestore?.[0];
const promise = this.$store.dispatch('management/findAll', { type: SNAPSHOT }).then((snapshots) => {
const toRestoreClusterName = cluster?.clusterName || cluster?.metadata?.name;
return snapshots.filter((s) => s?.snapshotFile?.status === STATES_ENUM.SUCCESSFUL && s.clusterName === toRestoreClusterName
);
});
// Map of snapshots by name
const allSnapshots = await promise.then((snapshots) => {
return snapshots.reduce((v, s) => {
v[s.name] = s;
return v;
}, {});
}).catch((err) => {
this.errors = exceptionToErrorsArray(err);
});
this.allSnapshots = allSnapshots;
this.sortedSnapshots = sortBy(Object.values(this.allSnapshots), ['snapshotFile.createdAt', 'created', 'metadata.creationTimestamp'], true);
},
selectDefaultSnapshot() {
if (this.selectedSnapshot) {
return;
}
const defaultSnapshot = this.toRestore[0]?.type === SNAPSHOT ? this.toRestore[0].name : this.clusterSnapshots[0]?.value;
this['selectedSnapshot'] = defaultSnapshot;
},
async apply(buttonDone) {
try {
const cluster = this.$store.getters['management/byId'](CAPI.RANCHER_CLUSTER, this.snapshot.clusterId);
await this.applyHooks(BEFORE_SAVE_HOOKS);
const now = cluster.spec?.rkeConfig?.etcdSnapshotRestore?.generation || 0;
set(cluster, 'spec.rkeConfig.etcdSnapshotRestore', {
generation: now + 1,
name: this.snapshot.name,
restoreRKEConfig: this.restoreMode,
});
await cluster.save();
this.$store.dispatch('growl/success', {
title: this.t('promptRestore.notification.title'),
message: this.t('promptRestore.notification.message', { selectedSnapshot: this.selectedSnapshot })
}, { root: true });
buttonDone(true);
this.close();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonDone(false);
}
},
snapshotLabel(snapshot) {
const dateFormat = escapeHtml(this.$store.getters['prefs/get'](DATE_FORMAT));
const timeFormat = escapeHtml( this.$store.getters['prefs/get'](TIME_FORMAT));
const created = snapshot.createdAt || snapshot.created || snapshot.metadata.creationTimestamp;
const d = day(created).format(dateFormat);
const t = day(created).format(timeFormat);
return `${ d } ${ t } : ${ snapshot.nameDisplay }`;
}
}
};
</script>
<template>
<app-modal
v-if="loaded"
custom-class="promptrestore-modal"
name="promptRestore"
styles="background-color: var(--nav-bg); border-radius: var(--border-radius); max-height: 100vh;"
height="auto"
:scrollable="true"
:trigger-focus-trap="true"
@close="close"
>
<Card
v-if="loaded"
class="prompt-restore"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('promptRestore.title', null, true)"
class="text-default-text"
/>
</template>
<template #body>
<div class="pl-10 pr-10">
<form>
<h3 v-t="'promptRestore.name'" />
<div v-if="!isCluster">
{{ snapshot.nameDisplay }}
</div>
<LabeledSelect
v-if="isCluster"
v-model:value="selectedSnapshot"
:label="t('promptRestore.label')"
:placeholder="t('promptRestore.placeholder')"
:options="clusterSnapshots"
/>
<div class="spacer" />
<h3 v-t="'promptRestore.date'" />
<div>
<p>
<Date
v-if="snapshot"
:value="snapshot.createdAt || snapshot.created || snapshot.metadata.creationTimestamp"
/>
</p>
</div>
<div class="spacer" />
<RadioGroup
v-model:value="restoreMode"
name="restoreMode"
label="Restore Type"
:labels="['Only etcd', 'Kubernetes version and etcd', 'Cluster config, Kubernetes version and etcd']"
:options="restoreModeOptions"
/>
</form>
</div>
</template>
<template #actions>
<div class="dialog-actions">
<button
class="btn role-secondary"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="restore"
:disabled="!hasSnapshot"
@click="apply"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
</app-modal>
</template>
<style lang='scss' scoped>
.promptrestore-modal {
border-radius: var(--border-radius);
overflow: scroll;
max-height: 100vh;
& ::-webkit-scrollbar-corner {
background: rgba(0,0,0,0);
}
.prompt-restore form p {
min-height: 16px;
}
// Position dialog buttons on the right-hand side of the dialog
.dialog-actions {
display: flex;
justify-content: flex-end;
}
}
.prompt-restore :deep() .card-wrap .card-actions {
display: block;
button:not(:last-child) {
margin-right: 10px;
}
.banner {
display: flex;
}
}
</style>