feat(dashboard): add support for importing/exporting cases

This commit is contained in:
Dragory 2021-11-03 00:05:53 +02:00
parent f3dae65747
commit 45941e47d6
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
9 changed files with 593 additions and 2 deletions

View file

@ -0,0 +1,159 @@
<template>
<div>
<h1>Import / Export</h1>
<p>
<strong>Note!</strong>
This feature is currently experimental. Make sure to always export a backup before importing server data. If you encounter any issues, please report them on the [Zeppelin Discord server](https://discord.gg/zeppelin).
</p>
<h2>Export server data</h2>
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': exportData, 'hover:bg-gray-800': !exportData }" @click="runExport()" :disabled="exportData">Create export file</button>
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': !exportData, 'hover:bg-gray-800': exportData }" :disabled="!exportData" @click="downloadExportFile">Download export file</button>
<p v-if="exportError">Error: {{ exportError }}</p>
<p v-else-if="exporting">Creating export file...</p>
<p v-else>&nbsp;</p>
<h2>Import server data</h2>
<p>
<strong>Note!</strong>
Always take a backup of your existing data above before importing.
</p>
<div class="mb-4">
<h3>Import file</h3>
<input type="file" @change="selectImportFile($event.target.files[0])">
</div>
<div class="mb-4">
<h3>Case options</h3>
<label><input type="radio" v-model="importCaseMode" value="bumpImportedCases"> Leave existing case numbers, start imported cases from the end</label><br>
<label><input type="radio" v-model="importCaseMode" value="bumpExistingCases"> Leave imported case numbers, re-number existing cases to start after imported cases</label><br>
<label><input type="radio" v-model="importCaseMode" value="replace"> Replace existing cases (!! THIS WILL DELETE ALL EXISTING CASES !!)</label>
</div>
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': importFile == null, 'hover:bg-gray-800': importFile != null }" @click="runImport()" :disabled="importFile == null">Import selected file</button>
<p v-if="importError">Error: {{ importError }}</p>
<p v-else-if="importing">Importing...</p>
<p v-else>&nbsp;</p>
</div>
</template>
<script lang="ts">
import { mapState } from "vuex";
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
import { AuthState, GuildState } from "../../store/types";
import { ApiError } from "../../api";
import moment from "moment";
export default {
async mounted() {
try {
await this.$store.dispatch("guilds/loadGuild", this.$route.params.guildId);
} catch (err) {
if (err instanceof ApiError) {
this.$router.push('/dashboard');
return;
}
throw err;
}
if (this.guild == null) {
this.$router.push('/dashboard');
return;
}
this.loading = false;
},
computed: {
...mapState("guilds", {
guild(guilds: GuildState) {
return guilds.available.get(this.$route.params.guildId);
},
}),
},
data() {
return {
loading: true,
importing: false,
importError: null,
importFile: null,
importCaseMode: "bumpImportedCases",
exporting: false,
exportError: null,
exportData: null,
};
},
methods: {
selectImportFile(file: File) {
this.importFile = file;
},
async runImport() {
if (this.importing) {
return;
}
if (! this.importFile) {
return;
}
this.importError = null;
this.importing = true;
try {
await this.$store.dispatch("guilds/importData", {
guildId: this.$route.params.guildId,
data: JSON.parse(await (this.importFile as File).text()),
caseHandlingMode: this.importCaseMode,
});
} catch (err) {
this.importError = String(err);
return;
} finally {
this.importing = false;
this.importFile = null;
}
window.alert("Data imported successfully!");
},
async runExport() {
if (this.exporting) {
return;
}
if (this.exportData) {
return;
}
this.exportError = null;
this.exporting = true;
try {
this.exportData = await this.$store.dispatch("guilds/exportData", {
guildId: this.$route.params.guildId,
});
} catch (err) {
this.exportError = String(err);
return;
} finally {
this.exporting = false;
}
},
downloadExportFile() {
if (!this.exportData) {
return;
}
const dl = document.createElement("a");
dl.setAttribute("href", `data:application/json,${encodeURIComponent(JSON.stringify(this.exportData, null, 2))}`);
dl.setAttribute("download", `export_${this.$route.params.guildId}_${moment().format("YYYY-MM-DD_HH-mm-ss")}.json`);
dl.style.display = "none";
document.body.appendChild(dl);
dl.click();
document.body.removeChild(dl);
},
},
};
</script>

View file

@ -19,6 +19,7 @@
<div class="pt-1">
<router-link class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/config'">Config</router-link>
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/access'">Access</router-link>
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/import-export'">Import/export</router-link>
</div>
</div>
</div>

View file

@ -86,6 +86,10 @@ export const router = new VueRouter({
path: "guilds/:guildId/access",
component: () => import("./components/dashboard/GuildAccess.vue"),
},
{
path: "guilds/:guildId/import-export",
component: () => import("./components/dashboard/GuildImportExport.vue"),
},
],
},
],

View file

@ -66,6 +66,17 @@ export const GuildStore: Module<GuildState, RootState> = {
await post(`guilds/${guildId}/set-target-permissions`, { guildId, targetId, type, permissions, expiresAt });
commit("setTargetPermissions", { guildId, targetId, type, permissions, expiresAt });
},
async importData({ commit }, { guildId, data, caseHandlingMode }) {
return post(`guilds/${guildId}/import`, {
data,
caseHandlingMode,
});
},
async exportData({ commit }, { guildId }) {
return post(`guilds/${guildId}/export`);
},
},
mutations: {