mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: ability to export connections
This commit is contained in:
@ -7,7 +7,7 @@
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-brush-variant mr-1" />
|
||||
<span class="cut-text">{{ t('connection.editConnectionAppearance') }}</span>
|
||||
<span class="cut-text">{{ t('application.editConnectionAppearance') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
|
@ -26,7 +26,7 @@
|
||||
type="text"
|
||||
required
|
||||
readonly
|
||||
:placeholder="t('database.schemaName')"
|
||||
@click.prevent="openPathDialog"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@ -251,7 +251,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-auto px-0">
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
<button class="btn btn-link mr-2" @click.stop="closeModal">
|
||||
{{ t('general.close') }}
|
||||
</button>
|
||||
<button
|
||||
|
@ -37,6 +37,13 @@
|
||||
>
|
||||
<a class="tab-link">{{ t('application.shortcuts') }}</a>
|
||||
</li>
|
||||
<li
|
||||
class="tab-item c-hand"
|
||||
:class="{'active': selectedTab === 'data'}"
|
||||
@click="selectTab('data')"
|
||||
>
|
||||
<a class="tab-link">{{ t('application.data') }}</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="updateStatus !== 'disabled'"
|
||||
class="tab-item c-hand"
|
||||
@ -366,6 +373,9 @@
|
||||
<div v-show="selectedTab === 'shortcuts'" class="panel-body py-4">
|
||||
<ModalSettingsShortcuts />
|
||||
</div>
|
||||
<div v-show="selectedTab === 'data'" class="panel-body py-4">
|
||||
<ModalSettingsData />
|
||||
</div>
|
||||
<div v-show="selectedTab === 'update'" class="panel-body py-4">
|
||||
<ModalSettingsUpdate />
|
||||
</div>
|
||||
@ -411,6 +421,7 @@ import { localesNames } from '@/i18n/supported-locales';
|
||||
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
|
||||
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
|
||||
import ModalSettingsShortcuts from '@/components/ModalSettingsShortcuts.vue';
|
||||
import ModalSettingsData from '@/components/ModalSettingsData.vue';
|
||||
import BaseTextEditor from '@/components/BaseTextEditor.vue';
|
||||
import BaseSelect from '@/components/BaseSelect.vue';
|
||||
import { AvailableLocale } from '@/i18n';
|
||||
@ -648,10 +659,14 @@ onBeforeUnmount(() => {
|
||||
.modal-body {
|
||||
overflow: hidden;
|
||||
|
||||
.tab-link {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
.tab-item {
|
||||
max-width: 20%;
|
||||
|
||||
.tab-link {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
|
84
src/renderer/components/ModalSettingsData.vue
Normal file
84
src/renderer/components/ModalSettingsData.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column col-12 h6 text-uppercase mb-2">
|
||||
{{ t('application.exportData') }}
|
||||
</div>
|
||||
<div class="column col-12">
|
||||
{{ t('application.exportDataExplanation') }}
|
||||
</div>
|
||||
<div class="column col-12 text-right">
|
||||
<button
|
||||
class="btn btn-primary d-inline-flex"
|
||||
@click="isExportModal=true"
|
||||
>
|
||||
<i class="mdi mdi-tray-arrow-up mdi-24px pr-2" />
|
||||
<span>{{ t('application.exportData') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns mt-4 mb-2">
|
||||
<div class="column col-12 h6 text-uppercase mb-2 mt-4">
|
||||
{{ t('application.importData') }}
|
||||
</div>
|
||||
<div class="column col-12">
|
||||
{{ t('application.importDataExplanation') }}
|
||||
</div>
|
||||
<div class="column col-12 text-right">
|
||||
<button
|
||||
class="btn btn-dark d-inline-flex ml-auto"
|
||||
@click="isImportModal=true"
|
||||
>
|
||||
<i class="mdi mdi-tray-arrow-down mdi-24px pr-2" />
|
||||
<span>{{ t('application.importData') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ModalSettingsDataExport
|
||||
v-if="isExportModal"
|
||||
@close="isExportModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-tray-arrow-up mr-1" /> {{ t('application.exportData') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
<!-- -->
|
||||
</div>
|
||||
</template>
|
||||
</ModalSettingsDataExport>
|
||||
|
||||
<!-- <ConfirmModal
|
||||
v-if="isImportModal"
|
||||
size="medium"
|
||||
@confirm="null"
|
||||
@hide="isImportModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-tray-arrow-down mr-1" /> {{ t('application.importData') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="mb-2">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmModal> -->
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
// import { useApplicationStore } from '@/stores/application';
|
||||
import ModalSettingsDataExport from '@/components/ModalSettingsDataExport.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isExportModal = ref(false);
|
||||
const isImportModal = ref(false);
|
||||
|
||||
</script>
|
327
src/renderer/components/ModalSettingsDataExport.vue
Normal file
327
src/renderer/components/ModalSettingsDataExport.vue
Normal file
@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div ref="trapRef" class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-tray-arrow-up mr-1" /> {{ t('application.exportData') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="columns export-options">
|
||||
<div class="column col-8 left">
|
||||
<div class="workspace-query-results" :style="'min-height: 300px;'">
|
||||
<div ref="table" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr text-center">
|
||||
<div class="th no-border" :style="'width:50%'" />
|
||||
<div class="th no-border" />
|
||||
<div class="th no-border">
|
||||
<label
|
||||
class="form-checkbox m-0 px-2 form-inline"
|
||||
@click.prevent="toggleAllConnections()"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:indeterminate="includeConnectionStatus === 2"
|
||||
:checked="!!includeConnectionStatus"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tr">
|
||||
<div class="th">
|
||||
<div class="table-column-title">
|
||||
<span>{{ t('connection.connectionName') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="table-column-title">
|
||||
<span>{{ t('connection.client') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th text-center">
|
||||
<div class="table-column-title">
|
||||
<span>{{ t('general.include') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbody">
|
||||
<div
|
||||
v-for="(item, i) in connections"
|
||||
:key="i"
|
||||
class="tr"
|
||||
>
|
||||
<div class="td">
|
||||
{{ getConnectionName(item.uid) }}
|
||||
</div>
|
||||
<div class="td">
|
||||
{{ item.client }}
|
||||
</div>
|
||||
<div class="td text-center">
|
||||
<label class="form-checkbox m-0 px-2 form-inline">
|
||||
<input v-model="connectionToggles[item.uid]" type="checkbox">
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4">
|
||||
<h5 class="h5">
|
||||
{{ t('general.options') }}
|
||||
</h5>
|
||||
<label class="form-checkbox">
|
||||
<input v-model="options.includes.passwords" type="checkbox">
|
||||
<i class="form-icon" />
|
||||
{{ t(`application.includeConnectionPasswords`) }}
|
||||
</label>
|
||||
<label class="form-checkbox">
|
||||
<input v-model="options.includes.folders" type="checkbox">
|
||||
<i class="form-icon" />
|
||||
{{ t(`application.includeFolders`) }}
|
||||
</label>
|
||||
<div class="h6 mt-4 mb-2">
|
||||
{{ t('application.encryptionPassword') }}
|
||||
</div>
|
||||
<fieldset class="form-group" :class="{'has-error': isPasswordError}">
|
||||
<div class="input-group">
|
||||
<input
|
||||
ref="passkey"
|
||||
v-model="options.passkey"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
class="form-input"
|
||||
:placeholder="t('application.required')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link input-group-addon"
|
||||
@click="isPasswordVisible = !isPasswordVisible"
|
||||
>
|
||||
<i v-if="isPasswordVisible" class="mdi mdi-eye px-1" />
|
||||
<i v-else class="mdi mdi-eye-off px-1" />
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="isPasswordError" class="form-input-hint">
|
||||
{{ t('application.encryptionPasswordError') }}
|
||||
</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-link mr-2" @click.stop="closeModal">
|
||||
{{ t('general.close') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
autofocus
|
||||
@click.prevent="exportData()"
|
||||
>
|
||||
{{ t('database.export') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
|
||||
import * as moment from 'moment';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import { useConnectionsStore } from '@/stores/connections';
|
||||
import { unproxify } from '@/libs/unproxify';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { encrypt } from 'common/libs/encrypter';
|
||||
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const { getConnectionName } = useConnectionsStore();
|
||||
const { connectionsOrder, connections } = storeToRefs(useConnectionsStore());
|
||||
const localConnections = unproxify<typeof connections.value>(connections.value);
|
||||
const localConnectionsOrder = unproxify<typeof connectionsOrder.value>(connectionsOrder.value);
|
||||
|
||||
const isPasswordVisible = ref(false);
|
||||
const isPasswordError = ref(false);
|
||||
const connectionToggles: Ref<{[k:string]: boolean}> = ref({});
|
||||
const options = ref({
|
||||
passkey: '',
|
||||
includes: {
|
||||
passwords: true,
|
||||
folders: true
|
||||
}
|
||||
});
|
||||
const filename = computed(() => {
|
||||
const date = moment().format('YYYY-MM-DD');
|
||||
return `backup_${date}`;
|
||||
});
|
||||
const includeConnectionStatus = computed(() => {
|
||||
if (Object.values(connectionToggles.value).every(item => item)) return 1;
|
||||
else if (Object.values(connectionToggles.value).some(item => item)) return 2;
|
||||
else return 0;
|
||||
});
|
||||
|
||||
const exportData = () => {
|
||||
if (options.value.passkey.length < 8)
|
||||
isPasswordError.value = true;
|
||||
else {
|
||||
isPasswordError.value = false;
|
||||
const connectionsToInclude: string[] = [];
|
||||
const connectionsUidMap = new Map<string, string>();
|
||||
for (const cUid in connectionToggles.value)
|
||||
if (connectionToggles.value[cUid]) connectionsToInclude.push(cUid);
|
||||
|
||||
let filteredConnections = unproxify<typeof localConnections>(localConnections.filter(conn => connectionsToInclude.includes(conn.uid)));
|
||||
filteredConnections = filteredConnections.map(c => {
|
||||
const newUid = uidGen('C');
|
||||
connectionsUidMap.set(c.uid, newUid);
|
||||
c.uid = newUid;
|
||||
return c;
|
||||
});
|
||||
|
||||
if (!options.value.includes.passwords) { // Remove passwords and set ask:true
|
||||
filteredConnections.map(c => {
|
||||
if (c.password) {
|
||||
c.password = '';
|
||||
c.ask = true;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
}
|
||||
|
||||
let filteredOrders = [];
|
||||
for (const [oldVal, newVal] of connectionsUidMap) {
|
||||
const connOrder = unproxify(localConnectionsOrder.find(c => c.uid === oldVal));
|
||||
connOrder.uid = newVal;
|
||||
filteredOrders.push(connOrder);
|
||||
}
|
||||
|
||||
if (options.value.includes.folders) { // Includes folders
|
||||
const oldConnUids = Array.from(connectionsUidMap.keys());
|
||||
const newConnUids = Array.from(connectionsUidMap.values());
|
||||
const foldersToInclude = unproxify(localConnectionsOrder).filter(f => (
|
||||
f.isFolder && oldConnUids.some(uid => f.connections.includes(uid))
|
||||
)).map(f => {
|
||||
f.connections = f.connections
|
||||
.map(fc => connectionsUidMap.get(fc))
|
||||
.filter(fc => newConnUids.includes(fc));
|
||||
return f;
|
||||
});
|
||||
|
||||
filteredOrders = [...filteredOrders, ...foldersToInclude];
|
||||
}
|
||||
|
||||
const exportObj = encrypt(JSON.stringify({
|
||||
connections: filteredConnections,
|
||||
connectionsOrder: filteredOrders
|
||||
}), options.value.passkey);
|
||||
|
||||
// console.log(exportObj, JSON.parse(decrypt(exportObj, options.value.passkey)));
|
||||
|
||||
const blobContent = Buffer.from(JSON.stringify(exportObj), 'utf-8').toString('hex');
|
||||
const file = new Blob([blobContent], { type: 'application/octet-stream' });
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.download = `${filename.value}.antares`;
|
||||
downloadLink.href = window.URL.createObjectURL(file);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = async () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const toggleAllConnections = () => {
|
||||
if (includeConnectionStatus.value !== 1) {
|
||||
connectionToggles.value = localConnections.reduce((acc, curr) => {
|
||||
acc[curr.uid] = true;
|
||||
return acc;
|
||||
}, {} as {[k:string]: boolean});
|
||||
}
|
||||
else {
|
||||
connectionToggles.value = localConnections.reduce((acc, curr) => {
|
||||
acc[curr.uid] = false;
|
||||
return acc;
|
||||
}, {} as {[k:string]: boolean});
|
||||
}
|
||||
};
|
||||
|
||||
connectionToggles.value = localConnections.reduce((acc, curr) => {
|
||||
acc[curr.uid] = true;
|
||||
return acc;
|
||||
}, {} as {[k:string]: boolean});
|
||||
|
||||
window.addEventListener('keydown', onKey);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKey);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.export-options {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-results {
|
||||
flex: 1 0 1px;
|
||||
|
||||
.table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
min-height: 0.8rem;
|
||||
padding: 0;
|
||||
|
||||
.form-icon {
|
||||
top: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
.modal-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
Reference in New Issue
Block a user