mirror of https://github.com/rancher/dashboard.git
307 lines
8.3 KiB
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>
|