mirror of https://github.com/Fabio286/antares.git
Merge branch 'feat/export-connections' of https://github.com/antares-sql/antares into develop
This commit is contained in:
commit
63544e95da
|
@ -0,0 +1,31 @@
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
const algorithm = 'aes-256-gcm';
|
||||||
|
|
||||||
|
function encrypt (text: string, password: string) {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const key = crypto.scryptSync(password, 'antares', 32);
|
||||||
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||||
|
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
return {
|
||||||
|
iv: iv.toString('hex'),
|
||||||
|
authTag: authTag.toString('hex'),
|
||||||
|
content: encrypted.toString('hex')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt (hash: { iv: string; content: string; authTag: string }, password: string) {
|
||||||
|
const key = crypto.scryptSync(password, 'antares', 32);
|
||||||
|
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(hash.iv, 'hex'));
|
||||||
|
decipher.setAuthTag(Buffer.from(hash.authTag, 'hex'));
|
||||||
|
const decrpyted = decipher.update(hash.content, 'hex', 'utf8') + decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrpyted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
encrypt,
|
||||||
|
decrypt
|
||||||
|
};
|
|
@ -16,6 +16,7 @@
|
||||||
:id="`id_${id}`"
|
:id="`id_${id}`"
|
||||||
class="file-uploader-input"
|
class="file-uploader-input"
|
||||||
type="file"
|
type="file"
|
||||||
|
:accept="accept"
|
||||||
@change="$emit('change', $event)"
|
@change="$emit('change', $event)"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -33,6 +34,10 @@ defineProps({
|
||||||
default: 'Browse',
|
default: 'Browse',
|
||||||
type: String
|
type: String
|
||||||
},
|
},
|
||||||
|
accept: {
|
||||||
|
default: '',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
default: '',
|
default: '',
|
||||||
type: String
|
type: String
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="modal-title h6">
|
<div class="modal-title h6">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<i class="mdi mdi-24px mdi-brush-variant mr-1" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
readonly
|
readonly
|
||||||
:placeholder="t('database.schemaName')"
|
@click.prevent="openPathDialog"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -251,7 +251,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-auto px-0">
|
<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') }}
|
{{ t('general.close') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -37,6 +37,13 @@
|
||||||
>
|
>
|
||||||
<a class="tab-link">{{ t('application.shortcuts') }}</a>
|
<a class="tab-link">{{ t('application.shortcuts') }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
class="tab-item c-hand"
|
||||||
|
:class="{'active': selectedTab === 'data'}"
|
||||||
|
@click="selectTab('data')"
|
||||||
|
>
|
||||||
|
<a class="tab-link">{{ t('application.data') }}</a>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="updateStatus !== 'disabled'"
|
v-if="updateStatus !== 'disabled'"
|
||||||
class="tab-item c-hand"
|
class="tab-item c-hand"
|
||||||
|
@ -366,6 +373,9 @@
|
||||||
<div v-show="selectedTab === 'shortcuts'" class="panel-body py-4">
|
<div v-show="selectedTab === 'shortcuts'" class="panel-body py-4">
|
||||||
<ModalSettingsShortcuts />
|
<ModalSettingsShortcuts />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="selectedTab === 'data'" class="panel-body py-4">
|
||||||
|
<ModalSettingsData />
|
||||||
|
</div>
|
||||||
<div v-show="selectedTab === 'update'" class="panel-body py-4">
|
<div v-show="selectedTab === 'update'" class="panel-body py-4">
|
||||||
<ModalSettingsUpdate />
|
<ModalSettingsUpdate />
|
||||||
</div>
|
</div>
|
||||||
|
@ -411,6 +421,7 @@ import { localesNames } from '@/i18n/supported-locales';
|
||||||
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
|
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
|
||||||
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
|
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
|
||||||
import ModalSettingsShortcuts from '@/components/ModalSettingsShortcuts.vue';
|
import ModalSettingsShortcuts from '@/components/ModalSettingsShortcuts.vue';
|
||||||
|
import ModalSettingsData from '@/components/ModalSettingsData.vue';
|
||||||
import BaseTextEditor from '@/components/BaseTextEditor.vue';
|
import BaseTextEditor from '@/components/BaseTextEditor.vue';
|
||||||
import BaseSelect from '@/components/BaseSelect.vue';
|
import BaseSelect from '@/components/BaseSelect.vue';
|
||||||
import { AvailableLocale } from '@/i18n';
|
import { AvailableLocale } from '@/i18n';
|
||||||
|
@ -648,11 +659,15 @@ onBeforeUnmount(() => {
|
||||||
.modal-body {
|
.modal-body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
max-width: 20%;
|
||||||
|
|
||||||
.tab-link {
|
.tab-link {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
min-height: calc(25vh - 70px);
|
min-height: calc(25vh - 70px);
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<ModalSettingsDataImport
|
||||||
|
v-if="isImportModal"
|
||||||
|
@close="isImportModal = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// import { useApplicationStore } from '@/stores/application';
|
||||||
|
import ModalSettingsDataExport from '@/components/ModalSettingsDataExport.vue';
|
||||||
|
import ModalSettingsDataImport from '@/components/ModalSettingsDataImport.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const isExportModal = ref(false);
|
||||||
|
const isImportModal = ref(false);
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,331 @@
|
||||||
|
<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 { SidebarElement, 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';
|
||||||
|
import { ConnectionParams } from 'common/interfaces/antares';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const { trapRef } = useFocusTrap();
|
||||||
|
|
||||||
|
const { getConnectionName } = useConnectionsStore();
|
||||||
|
const { connectionsOrder, connections } = storeToRefs(useConnectionsStore());
|
||||||
|
const localConnections = unproxify<ConnectionParams[]>(connections.value);
|
||||||
|
const localConnectionsOrder = unproxify<SidebarElement[]>(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.uid = uidGen('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();
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
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>
|
|
@ -0,0 +1,263 @@
|
||||||
|
<template>
|
||||||
|
<Teleport to="#window-content">
|
||||||
|
<div class="modal modal-sm 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-down mr-1" /> {{ t('application.importData') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body pb-0">
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="h6 mb-2">
|
||||||
|
{{ t('application.choseFile') }}
|
||||||
|
</div>
|
||||||
|
<BaseUploadInput
|
||||||
|
:model-value="filePath"
|
||||||
|
:message="t('general.browse')"
|
||||||
|
accept=".antares"
|
||||||
|
@clear="filePath = ''"
|
||||||
|
@change="filesChange($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="h6 mb-2">
|
||||||
|
{{ t('application.password') }}
|
||||||
|
</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 class="mb-2">
|
||||||
|
<label class="form-checkbox">
|
||||||
|
<input v-model="options.ignoreDuplicates" type="checkbox">
|
||||||
|
<i class="form-icon" />
|
||||||
|
{{ t(`application.ignoreDuplicates`) }}
|
||||||
|
</label>
|
||||||
|
</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"
|
||||||
|
:disabled="!filePath"
|
||||||
|
@click.prevent="importData()"
|
||||||
|
>
|
||||||
|
{{ t('database.import') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import BaseUploadInput from '@/components/BaseUploadInput.vue';
|
||||||
|
import { SidebarElement, useConnectionsStore } from '@/stores/connections';
|
||||||
|
import { unproxify } from '@/libs/unproxify';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { decrypt } from 'common/libs/encrypter';
|
||||||
|
import { useNotificationsStore } from '@/stores/notifications';
|
||||||
|
import { ConnectionParams } from 'common/interfaces/antares';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const { addNotification } = useNotificationsStore();
|
||||||
|
|
||||||
|
const connectionsStore = useConnectionsStore();
|
||||||
|
const { importConnections } = connectionsStore;
|
||||||
|
const { connections } = storeToRefs(connectionsStore);
|
||||||
|
|
||||||
|
const filePath = ref('');
|
||||||
|
const fileContent = ref(null);
|
||||||
|
const isPasswordVisible = ref(false);
|
||||||
|
const isPasswordError = ref(false);
|
||||||
|
const options = ref({
|
||||||
|
passkey: '',
|
||||||
|
ignoreDuplicates: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const filesChange = ({ target } : {target: HTMLInputElement }) => {
|
||||||
|
const { files } = target;
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(files[0]);
|
||||||
|
reader.onload = () => {
|
||||||
|
fileContent.value = reader.result;
|
||||||
|
filePath.value = files[0].path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const importData = () => {
|
||||||
|
if (options.value.passkey.length < 8)
|
||||||
|
isPasswordError.value = true;
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const hash = JSON.parse(Buffer.from(fileContent.value, 'hex').toString('utf-8'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const importObj: {
|
||||||
|
connections: ConnectionParams[];
|
||||||
|
connectionsOrder: SidebarElement[];
|
||||||
|
} = JSON.parse(decrypt(hash, options.value.passkey));
|
||||||
|
|
||||||
|
if (options.value.ignoreDuplicates) {
|
||||||
|
const actualConnections = unproxify(connections.value).map(c => {
|
||||||
|
delete c.uid;
|
||||||
|
|
||||||
|
delete c.name;
|
||||||
|
delete c.password;
|
||||||
|
delete c.ask;
|
||||||
|
|
||||||
|
delete c.key;
|
||||||
|
delete c.cert;
|
||||||
|
delete c.ca;
|
||||||
|
|
||||||
|
delete c.sshKey;
|
||||||
|
|
||||||
|
return JSON.stringify(c);
|
||||||
|
});
|
||||||
|
|
||||||
|
const incomingConnections = unproxify<ConnectionParams[]>(importObj.connections).map(c => {
|
||||||
|
const uid = c.uid;
|
||||||
|
delete c.uid;
|
||||||
|
|
||||||
|
delete c.name;
|
||||||
|
delete c.password;
|
||||||
|
delete c.ask;
|
||||||
|
|
||||||
|
delete c.key;
|
||||||
|
delete c.cert;
|
||||||
|
delete c.ca;
|
||||||
|
|
||||||
|
delete c.sshKey;
|
||||||
|
|
||||||
|
return { uid, jsonString: JSON.stringify(c) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const newConnectionsUid = incomingConnections
|
||||||
|
.filter(c => !actualConnections.includes(c.jsonString))
|
||||||
|
.reduce((acc, cur) => {
|
||||||
|
acc.push(cur.uid);
|
||||||
|
return acc;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
importObj.connections = importObj.connections.filter(c => newConnectionsUid.includes(c.uid));
|
||||||
|
importObj.connectionsOrder = importObj.connectionsOrder
|
||||||
|
.filter(c => newConnectionsUid
|
||||||
|
.includes(c.uid) ||
|
||||||
|
(c.isFolder && c.connections.every(c => newConnectionsUid.includes(c))));
|
||||||
|
}
|
||||||
|
|
||||||
|
importConnections(importObj);
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
status: 'success',
|
||||||
|
message: t('application.dataImportSuccess')
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
addNotification({
|
||||||
|
status: 'error',
|
||||||
|
message: t('application.wrongImportPassword')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
addNotification({
|
||||||
|
status: 'error',
|
||||||
|
message: t('application.wrongFileFormat')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Escape')
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
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-body {
|
||||||
|
max-height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -352,7 +352,6 @@ export const caES = {
|
||||||
csvStringDelimiter: 'Delimitador de cadena',
|
csvStringDelimiter: 'Delimitador de cadena',
|
||||||
csvIncludeHeader: 'Inclou capçalera',
|
csvIncludeHeader: 'Inclou capçalera',
|
||||||
csvExportOptions: 'Opcions d\'exportació CSV',
|
csvExportOptions: 'Opcions d\'exportació CSV',
|
||||||
scratchPadDefaultValue: '# COM SUPORTAR ANTARES\n\n- [ ] Deixa una estrella a Antares [repositori GitHub](https://github.com/antares-sql/antares)\n- [ ] Envia comentaris i consells\n- [ ] Informa d\'errors\n- [ ] Si t\'agrada, comparteix Antares amb amics\n\n# SOBRE EL BLOC DE NOTES\n\nAquest és un bloc de notes on pots guardar les teves **notes personals**. Suporta format `markdown`, però pots usar text pla.\nAquest contingut és simplement un espai reservat, pots esborrar-lo per fer lloc per les teves notes.\n',
|
|
||||||
phpArray: 'Matriu PHP'
|
phpArray: 'Matriu PHP'
|
||||||
},
|
},
|
||||||
faker: {
|
faker: {
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const enUS = {
|
||||||
new: 'New',
|
new: 'New',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
change: 'Change',
|
change: 'Change',
|
||||||
|
include: 'Include',
|
||||||
includes: 'Includes',
|
includes: 'Includes',
|
||||||
completed: 'Completed',
|
completed: 'Completed',
|
||||||
aborted: 'Aborted',
|
aborted: 'Aborted',
|
||||||
|
@ -292,6 +293,10 @@ export const enUS = {
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
fileName: 'File name',
|
fileName: 'File name',
|
||||||
|
choseFile: 'Chose file',
|
||||||
|
data: 'Data',
|
||||||
|
password: 'Password',
|
||||||
|
required: 'Required',
|
||||||
madeWithJS: 'Made with 💛 and JavaScript!',
|
madeWithJS: 'Made with 💛 and JavaScript!',
|
||||||
checkForUpdates: 'Check for updates',
|
checkForUpdates: 'Check for updates',
|
||||||
noUpdatesAvailable: 'No updates available',
|
noUpdatesAvailable: 'No updates available',
|
||||||
|
@ -358,7 +363,18 @@ export const enUS = {
|
||||||
csvStringDelimiter: 'String delimiter',
|
csvStringDelimiter: 'String delimiter',
|
||||||
csvIncludeHeader: 'Include header',
|
csvIncludeHeader: 'Include header',
|
||||||
csvExportOptions: 'CSV export options',
|
csvExportOptions: 'CSV export options',
|
||||||
scratchPadDefaultValue: '# HOW TO SUPPORT ANTARES\n\n- [ ] Leave a star to Antares [GitHub repo](https://github.com/antares-sql/antares)\n- [ ] Send feedbacks and advices\n- [ ] Report for bugs\n- [ ] If you enjoy, share Antares with friends\n\n# ABOUT SCRATCHPAD\n\nThis is a scratchpad where you can save your **personal notes**. It supports `markdown` format, but you are free to use plain text.\nThis content is just a placeholder, feel free to clear it to make space for your notes.\n'
|
exportData: 'Export data',
|
||||||
|
exportDataExplanation: 'Export saved connections to Antares. You will be asked for a password to encrypt the exported file.',
|
||||||
|
importData: 'Import data',
|
||||||
|
importDataExplanation: 'Imports an .antares file containing connections. You will need to enter the password defined during export.',
|
||||||
|
includeConnectionPasswords: 'Include connection passwords',
|
||||||
|
includeFolders: 'Include folders',
|
||||||
|
encryptionPassword: 'Encryption password',
|
||||||
|
encryptionPasswordError: 'The encryption password must be at least 8 characters long.',
|
||||||
|
ignoreDuplicates: 'Ignore duplicates',
|
||||||
|
wrongImportPassword: 'Wrong import password',
|
||||||
|
wrongFileFormat: 'Wrong file format',
|
||||||
|
dataImportSuccess: 'Data successfully imported'
|
||||||
},
|
},
|
||||||
faker: { // Faker.js methods, used in random generated content
|
faker: { // Faker.js methods, used in random generated content
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
|
|
@ -349,7 +349,6 @@ export const frFR = {
|
||||||
csvStringDelimiter: 'Séparateur',
|
csvStringDelimiter: 'Séparateur',
|
||||||
csvIncludeHeader: 'Inclure l\'en-tête',
|
csvIncludeHeader: 'Inclure l\'en-tête',
|
||||||
csvExportOptions: 'Options d\'export CSV',
|
csvExportOptions: 'Options d\'export CSV',
|
||||||
scratchPadDefaultValue: '# COMMENT SOUTENIR ANTARES\n\n- [ ] Laissez une etoile à Antares [GitHub repo](https://github.com/antares-sql/antares)\n- [ ] Envoyez vos avis et conseils\n- [ ] Signalez les bugs\n- [ ] Si vous l\'appréciez, partagez Antares avec des amis\n\n# A PROPOS DU BLOC-NOTES\n\nCeci est un bloc-notes où vous pouvez sauvegarder vos **notes personnelles**. Il supporte le format `markdown`, mais vous êtes libre d\'utiliser du texte standard.\nCe contenu est juste un modèle, vous pouvez a tout moment l\'effacer pour le remplacer par vos notes.\n',
|
|
||||||
phpArray: 'Tableau PHP'
|
phpArray: 'Tableau PHP'
|
||||||
},
|
},
|
||||||
faker: {
|
faker: {
|
||||||
|
|
|
@ -351,7 +351,6 @@ export const ptBR = {
|
||||||
csvStringDelimiter: 'Delimitador da String',
|
csvStringDelimiter: 'Delimitador da String',
|
||||||
csvIncludeHeader: 'Incluir cabeçalho',
|
csvIncludeHeader: 'Incluir cabeçalho',
|
||||||
csvExportOptions: 'Opções de exportação do CSV',
|
csvExportOptions: 'Opções de exportação do CSV',
|
||||||
scratchPadDefaultValue: '# COMO AJUDAR O ANTARES\n\n- [ ] Deixe sua estrela para o Antares [GitHub repo](https://github.com/antares-sql/antares)\n- [ ] Envie sugestões e avisos\n- [ ] Relate bugs\n- [ ] Se você gostar, compartilhe o Antares com amigos\n\n# SOBRE O BLOCO DE NOTAS\n\nEsse é o bloco de notas para salvar suas **notas pessoais**. ele suporta o formato `markdown`, porém você é livre para usar qualquer texto.\nEsse conteúdo é apenas um texto, fique a vontade para limpar e obter mais espaço para suas notas.\n',
|
|
||||||
phpArray: 'Array PHP'
|
phpArray: 'Array PHP'
|
||||||
},
|
},
|
||||||
faker: {
|
faker: {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { toRaw } from 'vue';
|
||||||
* @param {*} val
|
* @param {*} val
|
||||||
* @param {Boolean} json converts the value in JSON object (default true)
|
* @param {Boolean} json converts the value in JSON object (default true)
|
||||||
*/
|
*/
|
||||||
export function unproxify (val: any, json = true): any {
|
export function unproxify<T = any> (val: T, json = true): T {
|
||||||
if (json)// JSON conversion
|
if (json)// JSON conversion
|
||||||
return JSON.parse(JSON.stringify(val));
|
return JSON.parse(JSON.stringify(val));
|
||||||
else if (Array.isArray(val))// If array
|
else if (Array.isArray(val))// If array
|
||||||
|
|
|
@ -144,10 +144,16 @@
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group .input-group-addon {
|
.has-error .form-input {
|
||||||
|
background: $bg-color-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
.input-group-addon {
|
||||||
border-color: #3f3f3f;
|
border-color: #3f3f3f;
|
||||||
background: $bg-color-dark;
|
background: $bg-color-dark;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: $body-font-color-dark;
|
color: $body-font-color-dark;
|
||||||
|
|
|
@ -219,6 +219,16 @@ export const useConnectionsStore = defineStore('connections', {
|
||||||
|
|
||||||
this.connectionsOrder = (this.connectionsOrder as SidebarElement[]).filter(el => !emptyFolders.includes(el.uid));
|
this.connectionsOrder = (this.connectionsOrder as SidebarElement[]).filter(el => !emptyFolders.includes(el.uid));
|
||||||
persistentStore.set('connectionsOrder', this.connectionsOrder);
|
persistentStore.set('connectionsOrder', this.connectionsOrder);
|
||||||
|
},
|
||||||
|
importConnections (importObj: {
|
||||||
|
connections: ConnectionParams[];
|
||||||
|
connectionsOrder: SidebarElement[];
|
||||||
|
}) {
|
||||||
|
this.connections = [...this.connections, ...importObj.connections];
|
||||||
|
this.connectionsOrder = [...this.connectionsOrder, ...importObj.connectionsOrder];
|
||||||
|
|
||||||
|
persistentStore.set('connections', this.connections);
|
||||||
|
persistentStore.set('connectionsOrder', this.connectionsOrder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue