1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

refactor: ts and composition api on missing components

This commit is contained in:
2022-06-21 17:54:47 +02:00
parent 89e8d9fcdb
commit a103617ce8
38 changed files with 4553 additions and 4846 deletions

View File

@ -92,13 +92,15 @@ export interface TableInfos {
collation: string; collation: string;
} }
export type TableOptions = Partial<TableInfos>;
export interface TableField { export interface TableField {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
_antares_id?: string; _antares_id?: string;
name: string; name: string;
key: string;
type: string; type: string;
schema: string; schema: string;
table?: string;
numPrecision?: number; numPrecision?: number;
numLength?: number; numLength?: number;
datePrecision?: number; datePrecision?: number;
@ -109,6 +111,7 @@ export interface TableField {
zerofill?: boolean; zerofill?: boolean;
order?: number; order?: number;
default?: string; default?: string;
defaultType?: string;
enumValues?: string; enumValues?: string;
charset?: string; charset?: string;
collation?: string; collation?: string;
@ -118,7 +121,11 @@ export interface TableField {
comment?: string; comment?: string;
after?: string; after?: string;
orgName?: string; orgName?: string;
length?: number; length?: number | false;
alias: string;
tableAlias: string;
orgTable: string;
key?: 'pri' | 'uni';
} }
export interface TableIndex { export interface TableIndex {
@ -136,6 +143,8 @@ export interface TableIndex {
} }
export interface TableForeign { export interface TableForeign {
// eslint-disable-next-line camelcase
_antares_id?: string;
constraintName: string; constraintName: string;
refSchema: string; refSchema: string;
table: string; table: string;
@ -147,15 +156,6 @@ export interface TableForeign {
oldName?: string; oldName?: string;
} }
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams { export interface CreateTableParams {
/** Connection UID */ /** Connection UID */
uid?: string; uid?: string;
@ -236,15 +236,12 @@ export interface CreateTriggerParams {
export interface AlterTriggerParams extends CreateTriggerParams { export interface AlterTriggerParams extends CreateTriggerParams {
oldName?: string; oldName?: string;
} }
export interface TriggerFunctionInfos {
name: string;
type: string;
security: string;
}
// Routines & Functions // Routines & Functions
export interface FunctionParam { export interface FunctionParam {
// eslint-disable-next-line camelcase
_antares_id: string;
context: string; context: string;
name: string; name: string;
type: string; type: string;
@ -267,9 +264,11 @@ export interface RoutineInfos {
parameters?: FunctionParam[]; parameters?: FunctionParam[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
returns?: any; returns?: any;
returnsLength?: number;
} }
export type FunctionInfos = RoutineInfos export type FunctionInfos = RoutineInfos
export type TriggerFunctionInfos = FunctionInfos
export interface CreateRoutineParams { export interface CreateRoutineParams {
name: string; name: string;
@ -309,29 +308,6 @@ export interface AlterFunctionParams extends CreateFunctionParams {
// Events // Events
export interface EventInfos { export interface EventInfos {
name: string;
definition: string;
type: string;
definer: string;
body: string;
starts: string;
ends: string;
enabled: boolean;
executeAt: string;
intervalField: string;
intervalValue: string;
onCompletion: string;
originator: string;
sqlMode: string;
created: string;
updated: string;
lastExecuted: string;
comment: string;
charset: string;
timezone: string;
}
export interface CreateEventParams {
definer?: string; definer?: string;
schema: string; schema: string;
name: string; name: string;
@ -340,12 +316,15 @@ export interface CreateEventParams {
starts: string; starts: string;
ends: string; ends: string;
at: string; at: string;
preserve: string; preserve: boolean;
state: string; state: string;
comment: string; comment: string;
enabled?: boolean;
sql: string; sql: string;
} }
export type CreateEventParams = EventInfos;
export interface AlterEventParams extends CreateEventParams { export interface AlterEventParams extends CreateEventParams {
oldName?: string; oldName?: string;
} }
@ -397,17 +376,10 @@ export interface QueryParams {
tabUid?: string; tabUid?: string;
} }
export interface QueryField { /**
name: string; * @deprecated Use TableFIeld
alias: string; */
orgName: string; export type QueryField = TableField
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
export interface QueryForeign { export interface QueryForeign {
schema: string; schema: string;

View File

@ -1,5 +1,34 @@
import { UsableLocale } from '@faker-js/faker'; import { UsableLocale } from '@faker-js/faker';
export interface TableUpdateParams {
uid: string;
schema: string;
table: string;
primary?: string;
id: number | string;
content: number | string | boolean | Date | Blob | null;
type: string;
field: string;
}
export interface TableDeleteParams {
uid: string;
schema: string;
table: string;
primary?: string;
field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any};
}
export interface TableFilterClausole {
active: boolean;
field: string;
op: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL';
value: '';
value2: '';
}
export interface InsertRowsParams { export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;

View File

@ -381,25 +381,18 @@ export class MySQLClient extends AntaresCore {
const remappedSchedulers: antares.EventInfos[] = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => { const remappedSchedulers: antares.EventInfos[] = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return { return {
name: scheduler.EVENT_NAME, name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION, schema: scheduler.Db,
type: scheduler.EVENT_TYPE, sql: scheduler.EVENT_DEFINITION,
execution: scheduler.EVENT_TYPE === 'RECURRING' ? 'EVERY' : 'ONCE',
definer: scheduler.DEFINER, definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
state: scheduler.STATUS === 'ENABLED' ? 'ENABLE' : scheduler.STATE === 'DISABLED' ? 'DISABLE' : 'DISABLE ON SLAVE',
enabled: scheduler.STATUS === 'ENABLED', enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT, at: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD, every: [scheduler.INTERVAL_FIELD, scheduler.INTERVAL_VALUE],
intervalValue: scheduler.INTERVAL_VALUE, preserve: scheduler.ON_COMPLETION.includes('PRESERVE'),
onCompletion: scheduler.ON_COMPLETION, comment: scheduler.EVENT_COMMENT
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
}; };
}); });
@ -930,19 +923,22 @@ export class MySQLClient extends AntaresCore {
} }
async getViewInformations ({ schema, view }: { schema: string; view: string }) { async getViewInformations ({ schema, view }: { schema: string; view: string }) {
const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``; const { rows: algorithm } = await this.raw(`SHOW CREATE VIEW \`${schema}\`.\`${view}\``);
const results = await this.raw(sql); const { rows: viewInfo } = await this.raw(`
SELECT *
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = '${schema}'
AND TABLE_NAME = '${view}'
`);
return results.rows.map(row => {
return { return {
algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0], algorithm: algorithm[0]['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: viewInfo[0].DEFINER,
security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0], security: viewInfo[0].SECURITY_TYPE,
updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '', updateOption: viewInfo[0].CHECK_OPTION === 'NONE' ? '' : viewInfo[0].CHECK_OPTION,
sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0], sql: viewInfo[0].VIEW_DEFINITION,
name: row.View name: viewInfo[0].TABLE_NAME
}; };
})[0];
} }
async dropView (params: { schema: string; view: string }) { async dropView (params: { schema: string; view: string }) {
@ -955,7 +951,7 @@ export class MySQLClient extends AntaresCore {
USE \`${view.schema}\`; USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security} SQL SECURITY ${view.security}
params \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''} VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`; `;
if (view.name !== view.oldName) if (view.name !== view.oldName)

View File

@ -50,14 +50,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, PropType, Ref, ref } from 'vue'; import { computed, PropType, Ref, ref } from 'vue';
import { NUMBER, FLOAT } from 'common/fieldTypes'; import { NUMBER, FLOAT } from 'common/fieldTypes';
import { FunctionParam } from 'common/interfaces/antares'; import { FunctionInfos, RoutineInfos } from 'common/interfaces/antares';
import ConfirmModal from '@/components/BaseConfirmModal.vue'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
// eslint-disable-next-line camelcase
type LocalRoutineParams = FunctionParam & {_antares_id: string};
const props = defineProps({ const props = defineProps({
localRoutine: Object as PropType<{name: string; parameters: LocalRoutineParams[]}>, localRoutine: Object as PropType<RoutineInfos | FunctionInfos>,
client: String client: String
}); });

View File

@ -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-history mr-1" /> <i class="mdi mdi-24px mdi-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span> <span class="cut-text">{{ t('word.history') }}: {{ connectionName }}</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" />
@ -22,7 +22,7 @@
v-model="searchTerm" v-model="searchTerm"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('message.searchForQueries')" :placeholder="t('message.searchForQueries')"
> >
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" /> <i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i <i
@ -67,13 +67,13 @@
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small> <small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons"> <div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)"> <button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }} <i class="mdi mdi-open-in-app pr-1" /> {{ t('word.select') }}
</button> </button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)"> <button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }} <i class="mdi mdi-content-copy pr-1" /> {{ t('word.copy') }}
</button> </button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)"> <button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }} <i class="mdi mdi-delete-forever pr-1" /> {{ t('word.delete') }}
</button> </button>
</div> </div>
</div> </div>
@ -88,7 +88,7 @@
<i class="mdi mdi-history mdi-48px" /> <i class="mdi mdi-history mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }} {{ t('message.thereIsNoQueriesYet') }}
</p> </p>
</div> </div>
</div> </div>
@ -99,12 +99,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue'; import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as moment from 'moment'; import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { HistoryRecord, useHistoryStore } from '@/stores/history'; import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
const { t } = useI18n();
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore(); const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore(); const { getConnectionName } = useConnectionsStore();

View File

@ -11,27 +11,27 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -42,7 +42,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -55,7 +55,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -67,11 +67,11 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
@ -81,7 +81,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.returns') }} {{ t('word.returns') }}
</label> </label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
@ -101,7 +101,7 @@
class="form-input" class="form-input"
type="number" type="number"
min="0" min="0"
:placeholder="$t('word.length')" :placeholder="t('word.length')"
> >
</div> </div>
</div> </div>
@ -109,7 +109,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -121,7 +121,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.security" v-model="localFunction.security"
@ -133,7 +133,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto"> <div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.dataAccess" v-model="localFunction.dataAccess"
@ -146,7 +146,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -154,7 +154,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -186,6 +186,9 @@ import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue'; import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionInfos, FunctionParam } from 'common/interfaces/antares'; import { FunctionInfos, FunctionParam } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabUid: String, tabUid: String,

View File

@ -201,7 +201,7 @@ const {
changeBreadcrumbs changeBreadcrumbs
} = workspacesStore; } = workspacesStore;
const indexTable: Ref<Component & {$refs: { tableWrapper: HTMLDivElement }}> = ref(null); const indexTable: Ref<Component & { tableWrapper: HTMLDivElement }> = ref(null);
const firstInput: Ref<HTMLInputElement> = ref(null); const firstInput: Ref<HTMLInputElement> = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
@ -336,7 +336,7 @@ const addField = () => {
}); });
setTimeout(() => { setTimeout(() => {
const scrollable = indexTable.value.$refs.tableWrapper; const scrollable = indexTable.value.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 20);
}; };
@ -364,7 +364,7 @@ const duplicateField = (uid: string) => {
localFields.value = [...localFields.value, fieldToClone]; localFields.value = [...localFields.value, fieldToClone];
setTimeout(() => { setTimeout(() => {
const scrollable = indexTable.value.$refs.tableWrapper; const scrollable = indexTable.value.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 20);
}; };

View File

@ -85,12 +85,12 @@
/> />
<div v-if="customizations.triggerMultipleEvents" class="px-4"> <div v-if="customizations.triggerMultipleEvents" class="px-4">
<label <label
v-for="event in Object.keys(localEvents)" v-for="event in Object.keys(localEvents) as ('INSERT' | 'UPDATE' | 'DELETE')[]"
:key="event" :key="event"
class="form-checkbox form-inline" class="form-checkbox form-inline"
@change.prevent="changeEvents(event as 'INSERT' | 'UPDATE' | 'DELETE')" @change.prevent="changeEvents(event)"
> >
<input :checked="localEvents[event as 'INSERT' | 'UPDATE' | 'DELETE']" type="checkbox"><i class="form-icon" /> {{ event }} <input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label> </label>
</div> </div>
</div> </div>

View File

@ -144,17 +144,9 @@ const originalView = ref(null);
const localView = ref(null); const localView = ref(null);
const editorHeight = ref(300); const editorHeight = ref(300);
const workspace = computed(() => { const workspace = computed(() => getWorkspace(props.connection.uid));
return getWorkspace(props.connection.uid); const isChanged = computed(() => JSON.stringify(originalView.value) !== JSON.stringify(localView.value));
}); const isDefinerInUsers = computed(() => originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true);
const isChanged = computed(() => {
return JSON.stringify(originalView.value) !== JSON.stringify(localView.value);
});
const isDefinerInUsers = computed(() => {
return originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
});
const users = computed(() => { const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users]; const users = [{ value: '' }, ...workspace.value.users];

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runFunctionCheck" @click="runFunctionCheck"
> >
<i class="mdi mdi-24px mdi-play mr-1" /> <i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span> <span>{{ t('word.run') }}</span>
</button> </button>
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -90,13 +90,13 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.returns') }} {{ t('word.returns') }}
</label> </label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
v-model="localFunction.returns" v-model="localFunction.returns"
class="form-select text-uppercase" class="form-select text-uppercase"
:options="[{ name: 'VOID' }, ...workspace.dataTypes]" :options="[{ name: 'VOID' }, ...(workspace.dataTypes as any)]"
group-label="group" group-label="group"
group-values="types" group-values="types"
option-label="name" option-label="name"
@ -110,7 +110,7 @@
class="form-input" class="form-input"
type="number" type="number"
min="0" min="0"
:placeholder="$t('word.length')" :placeholder="t('word.length')"
> >
</div> </div>
</div> </div>
@ -118,7 +118,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -130,7 +130,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.security" v-model="localFunction.security"
@ -142,7 +142,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto"> <div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.dataAccess" v-model="localFunction.dataAccess"
@ -155,7 +155,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -163,7 +163,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -191,40 +191,34 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal'; import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters'; import ModalAskParameters from '@/components/ModalAskParameters.vue';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
import { AlterFunctionParams, FunctionInfos, FunctionParam } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsFunction',
components: { const props = defineProps({
BaseLoader,
QueryEditor,
WorkspaceTabPropsFunctionParamsModal,
ModalAskParameters,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
function: String, function: String,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
@ -234,257 +228,236 @@ export default {
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const firstInput: Ref<HTMLInputElement> = ref(null);
selectedWorkspace, const isLoading = ref(false);
getWorkspace, const isSaving = ref(false);
refreshStructure, const isParamsModal = ref(false);
renameTabs, const isAskingParameters = ref(false);
newTab, const originalFunction: Ref<FunctionInfos> = ref(null);
changeBreadcrumbs, const localFunction: Ref<FunctionInfos> = ref({ name: '', sql: '', definer: null });
setUnsavedChanges const lastFunction = ref(null);
}; const sqlProxy = ref('');
}, const editorHeight = ref(300);
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
isTableNameValid () {
return this.localFunction.name !== '';
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localFunction.returns);
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const workspace = computed(() => {
} return getWorkspace(props.connection.uid);
}, });
watch: {
async schema () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async function () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, function: this.function });
setTimeout(() => { const customizations = computed(() => {
this.resizeQueryEditor(); return workspace.value.customizations;
}, 200); });
if (this.lastFunction !== this.function) const isChanged = computed(() => {
this.getRoutineData(); return JSON.stringify(originalFunction.value) !== JSON.stringify(localFunction.value);
} });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getFunctionData () {
if (!this.function) return;
this.isLoading = true; const isTableNameValid = computed(() => {
this.localFunction = { sql: '' }; return localFunction.value.name !== '';
this.lastFunction = this.function; });
const getFunctionData = async () => {
if (!props.function) return;
isLoading.value = true;
localFunction.value = { name: '', sql: '', definer: null };
lastFunction.value = props.function;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
func: this.function func: props.function
}; };
try { try {
const { status, response } = await Functions.getFunctionInformations(params); const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalFunction = response; originalFunction.value = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => { originalFunction.value.parameters = [...originalFunction.value.parameters.map(param => {
param._antares_id = uidGen(); param._antares_id = uidGen();
return param; return param;
})]; })];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
this.sqlProxy = this.localFunction.sql; sqlProxy.value = localFunction.value.sql;
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
func: { func: {
...this.localFunction, ...localFunction.value,
schema: this.schema, schema: props.schema,
oldName: this.originalFunction.name oldName: originalFunction.value.name
} } as AlterFunctionParams
}; };
try { try {
const { status, response } = await Functions.alterFunction(params); const { status, response } = await Functions.alterFunction(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.originalFunction.name; const oldName = originalFunction.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localFunction.name) { if (oldName !== localFunction.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localFunction.name, elementNewName: localFunction.value.name,
elementType: 'function' elementType: 'function'
}); });
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name }); changeBreadcrumbs({ schema: props.schema, function: localFunction.value.name });
} }
else else
this.getFunctionData(); getFunctionData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); const clearChanges = () => {
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql); localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
}, queryEditor.value.editor.session.setValue(localFunction.value.sql);
resizeQueryEditor () { };
if (this.$refs.queryEditor) {
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
optionsUpdate (options) {
this.localFunction = options; const parametersUpdate = (parameters: FunctionParam[]) => {
}, localFunction.value = { ...localFunction.value, parameters };
parametersUpdate (parameters) { };
this.localFunction = { ...this.localFunction, parameters };
}, const runFunctionCheck = () => {
runFunctionCheck () { if (localFunction.value.parameters.length)
if (this.localFunction.parameters.length) showAskParamsModal();
this.showAskParamsModal();
else else
this.runFunction(); runFunction();
}, };
runFunction (params) {
const runFunction = (params?: string[]) => {
if (!params) params = []; if (!params) params = [];
let sql; let sql;
switch (this.connection.client) { // TODO: move in a better place switch (props.connection.client) { // TODO: move in a better place
case 'maria': case 'maria':
case 'mysql': case 'mysql':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`; sql = `SELECT \`${originalFunction.value.name}\` (${params.join(',')})`;
break; break;
case 'pg': case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`; sql = `SELECT ${originalFunction.value.name}(${params.join(',')})`;
break; break;
case 'mssql': case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`; sql = `SELECT ${originalFunction.value.name} ${params.join(',')}`;
break; break;
default: default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`; sql = `SELECT \`${originalFunction.value.name}\` (${params.join(',')})`;
} }
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true }); newTab({ uid: props.connection.uid, content: sql, type: 'query', autorun: true });
}, };
showParamsModal () {
this.isParamsModal = true; const showParamsModal = () => {
}, isParamsModal.value = true;
hideParamsModal () { };
this.isParamsModal = false;
}, const hideParamsModal = () => {
showAskParamsModal () { isParamsModal.value = false;
this.isAskingParameters = true; };
},
hideAskParamsModal () { const showAskParamsModal = () => {
this.isAskingParameters = false; isAskingParameters.value = true;
}, };
onKey (e) {
if (this.isSelected) { const hideAskParamsModal = () => {
isAskingParameters.value = false;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.function, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, function: props.function });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastFunction.value !== props.function)
getFunctionData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span> <span class="cut-text">{{ t('word.parameters') }} "{{ func }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter"> <button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" /> <i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -74,7 +74,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -86,7 +86,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.type') }} {{ t('word.type') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -102,7 +102,7 @@
</div> </div>
<div v-if="customizations.parametersLength" class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ t('word.length') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -115,7 +115,7 @@
</div> </div>
<div v-if="customizations.functionContext" class="form-group"> <div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ t('word.context') }}
</label> </label>
<div class="column"> <div class="column">
<label class="form-radio"> <label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" /> <i class="mdi mdi-dots-horizontal mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }} {{ t('message.thereAreNoParameters') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addParameter"> <button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }} {{ t('message.createNewParameter') }}
</button> </button>
</div> </div>
</div> </div>
@ -164,113 +164,118 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsFunctionParamsModal',
components: { const props = defineProps({
ConfirmModal,
BaseSelect
},
props: {
localParameters: { localParameters: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
func: String, func: String,
workspace: Object workspace: Object
}, });
emits: ['hide', 'parameters-update'],
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length) const emit = defineEmits(['hide', 'parameters-update']);
this.resetSelectedID();
this.getModalInnerHeight(); const parametersPanel: Ref<HTMLDivElement> = ref(null);
window.addEventListener('resize', this.getModalInnerHeight); const parametersProxy = ref([]);
}, const selectedParam = ref('');
unmounted () { const modalInnerHeight = ref(400);
window.removeEventListener('resize', this.getModalInnerHeight); const i = ref(1);
},
methods: { const selectedParamObj = computed(() => {
typeClass (type) { return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
});
const isChanged = computed(() => {
return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
});
const customizations = computed(() => {
return props.workspace.customizations;
});
const typeClass = (type: string) => {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return ''; return '';
}, };
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); const confirmParametersChange = () => {
}, emit('parameters-update', parametersProxy.value);
selectParameter (event, uid) { };
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid; const selectParameter = (event: MouseEvent, uid: string) => {
}, // eslint-disable-next-line @typescript-eslint/no-explicit-any
getModalInnerHeight () { if (selectedParam.value !== uid && !(event.target as any).classList.contains('remove-field'))
selectedParam.value = uid;
};
const getModalInnerHeight = () => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
if (modalBody) if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, };
addParameter () {
const addParameter = () => {
const newUid = uidGen(); const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, { parametersProxy.value = [...parametersProxy.value, {
_antares_id: newUid, _antares_id: newUid,
name: `param${this.i++}`, name: `param${i.value++}`,
type: this.workspace.dataTypes[0].types[0].name, type: props.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: '' length: ''
}]; }];
if (this.parametersProxy.length === 1) if (parametersProxy.value.length === 1)
this.resetSelectedID(); resetSelectedID();
setTimeout(() => { setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; parametersPanel.value.scrollTop = parametersPanel.value.scrollHeight + 60;
this.selectedParam = newUid; selectedParam.value = newUid;
}, 20); }, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
}
}
}; };
const removeParameter = (uid: string) => {
parametersProxy.value = parametersProxy.value.filter(param => param._antares_id !== uid);
if (parametersProxy.value.length && selectedParam.value === uid)
resetSelectedID();
};
const clearChanges = () => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (!parametersProxy.value.some(param => param.name === selectedParam.value))
resetSelectedID();
};
const resetSelectedID = () => {
selectedParam.value = parametersProxy.value.length ? parametersProxy.value[0]._antares_id : '';
};
onMounted(() => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (parametersProxy.value.length)
resetSelectedID();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runRoutineCheck" @click="runRoutineCheck"
> >
<i class="mdi mdi-24px mdi-play mr-1" /> <i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span> <span>{{ t('word.run') }}</span>
</button> </button>
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.language" v-model="localRoutine.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.definer" v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name: t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -90,7 +90,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localRoutine.comment" v-model="localRoutine.comment"
@ -102,7 +102,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.security" v-model="localRoutine.security"
@ -114,7 +114,7 @@
<div v-if="customizations.procedureDataAccess" class="column col-auto"> <div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.dataAccess" v-model="localRoutine.dataAccess"
@ -127,7 +127,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -135,7 +135,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label> <label class="form-label ml-2">{{ t('message.routineBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -163,40 +163,34 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onUnmounted, onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue';
import { AlterRoutineParams, FunctionParam, RoutineInfos } from 'common/interfaces/antares';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsRoutine',
components: { const props = defineProps({
QueryEditor,
BaseLoader,
WorkspaceTabPropsRoutineParamsModal,
ModalAskParameters,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
routine: String, routine: String,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
@ -206,243 +200,235 @@ export default {
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const firstInput: Ref<HTMLInputElement> = ref(null);
selectedWorkspace, const isLoading = ref(false);
getWorkspace, const isSaving = ref(false);
refreshStructure, const isParamsModal = ref(false);
renameTabs, const isAskingParameters = ref(false);
newTab, const originalRoutine: Ref<RoutineInfos> = ref(null);
changeBreadcrumbs, const localRoutine: Ref<RoutineInfos> = ref(null);
setUnsavedChanges const lastRoutine = ref(null);
}; const sqlProxy = ref('');
}, const editorHeight = ref(300);
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
isTableNameValid () {
return this.localRoutine.name !== '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const workspace = computed(() => {
} return getWorkspace(props.connection.uid);
}, });
watch: {
async schema () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async routine () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, routine: this.routine });
setTimeout(() => { const customizations = computed(() => {
this.resizeQueryEditor(); return workspace.value.customizations;
}, 200); });
if (this.lastRoutine !== this.routine) const isChanged = computed(() => {
this.getRoutineData(); return JSON.stringify(originalRoutine.value) !== JSON.stringify(localRoutine.value);
} });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getRoutineData () {
if (!this.routine) return;
this.localRoutine = { sql: '' }; const isTableNameValid = computed(() => {
this.isLoading = true; return localRoutine.value.name !== '';
this.lastRoutine = this.routine; });
const getRoutineData = async () => {
if (!props.routine) return;
localRoutine.value = { name: '', sql: '', definer: null };
isLoading.value = true;
lastRoutine.value = props.routine;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
routine: this.routine routine: props.routine
}; };
try { try {
const { status, response } = await Routines.getRoutineInformations(params); const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalRoutine = response; originalRoutine.value = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => { originalRoutine.value.parameters = [...originalRoutine.value.parameters.map(param => {
param._antares_id = uidGen(); param._antares_id = uidGen();
return param; return param;
})]; })];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine)); localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
this.sqlProxy = this.localRoutine.sql; sqlProxy.value = localRoutine.value.sql;
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid as string,
routine: { routine: {
...this.localRoutine, ...localRoutine.value,
schema: this.schema, schema: props.schema,
oldName: this.originalRoutine.name oldName: originalRoutine.value.name
} } as AlterRoutineParams
}; };
try { try {
const { status, response } = await Routines.alterRoutine(params); const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.originalRoutine.name; const oldName = originalRoutine.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localRoutine.name) { if (oldName !== localRoutine.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localRoutine.name, elementNewName: localRoutine.value.name,
elementType: 'procedure' elementType: 'procedure'
}); });
this.changeBreadcrumbs({ schema: this.schema, procedure: this.localRoutine.name }); changeBreadcrumbs({ schema: props.schema, routine: localRoutine.value.name });
} }
else else
this.getRoutineData(); getRoutineData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine)); const clearChanges = () => {
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql); localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
}, queryEditor.value.editor.session.setValue(localRoutine.value.sql);
resizeQueryEditor () { };
if (this.$refs.queryEditor) {
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
optionsUpdate (options) {
this.localRoutine = options; const parametersUpdate = (parameters: FunctionParam[]) => {
}, localRoutine.value = { ...localRoutine.value, parameters };
parametersUpdate (parameters) { };
this.localRoutine = { ...this.localRoutine, parameters };
}, const runRoutineCheck = () => {
runRoutineCheck () { if (localRoutine.value.parameters.length)
if (this.localRoutine.parameters.length) showAskParamsModal();
this.showAskParamsModal();
else else
this.runRoutine(); runRoutine();
}, };
runRoutine (params) {
const runRoutine = (params?: string[]) => {
if (!params) params = []; if (!params) params = [];
let sql; let sql;
switch (this.connection.client) { // TODO: move in a better place switch (props.connection.client) { // TODO: move in a better place
case 'maria': case 'maria':
case 'mysql': case 'mysql':
case 'pg': case 'pg':
sql = `CALL ${this.originalRoutine.name}(${params.join(',')})`; sql = `CALL ${originalRoutine.value.name}(${params.join(',')})`;
break; break;
case 'mssql': case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`; sql = `EXEC ${originalRoutine.value.name} ${params.join(',')}`;
break; break;
default: default:
sql = `CALL \`${this.originalRoutine.name}\`(${params.join(',')})`; sql = `CALL \`${originalRoutine.value.name}\`(${params.join(',')})`;
} }
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true }); newTab({ uid: props.connection.uid, content: sql, type: 'query', autorun: true });
}, };
showParamsModal () {
this.isParamsModal = true; const showParamsModal = () => {
}, isParamsModal.value = true;
hideParamsModal () { };
this.isParamsModal = false;
}, const hideParamsModal = () => {
showAskParamsModal () { isParamsModal.value = false;
this.isAskingParameters = true; };
},
hideAskParamsModal () { const showAskParamsModal = () => {
this.isAskingParameters = false; isAskingParameters.value = true;
}, };
onKey (e) {
if (this.isSelected) { const hideAskParamsModal = () => {
isAskingParameters.value = false;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
lastRoutine.value = props.routine;
}
});
watch(() => props.routine, async () => {
if (props.isSelected) {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
lastRoutine.value = props.routine;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, routine: props.routine });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastRoutine.value !== props.routine)
getRoutineData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span> <span class="cut-text">{{ t('word.parameters') }} "{{ routine }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter"> <button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" /> <i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -74,7 +74,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -86,7 +86,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.type') }} {{ t('word.type') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -102,7 +102,7 @@
</div> </div>
<div v-if="customizations.parametersLength" class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ t('word.length') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -115,7 +115,7 @@
</div> </div>
<div v-if="customizations.procedureContext" class="form-group"> <div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ t('word.context') }}
</label> </label>
<div class="column"> <div class="column">
<label class="form-radio"> <label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" /> <i class="mdi mdi-dots-horizontal mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }} {{ t('message.thereAreNoParameters') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addParameter"> <button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }} {{ t('message.createNewParameter') }}
</button> </button>
</div> </div>
</div> </div>
@ -164,113 +164,118 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsRoutineParamsModal',
components: { const props = defineProps({
ConfirmModal,
BaseSelect
},
props: {
localParameters: { localParameters: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
routine: String, routine: String,
workspace: Object workspace: Object
}, });
emits: ['parameters-update', 'hide'],
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length) const emit = defineEmits(['hide', 'parameters-update']);
this.resetSelectedID();
this.getModalInnerHeight(); const parametersPanel: Ref<HTMLDivElement> = ref(null);
window.addEventListener('resize', this.getModalInnerHeight); const parametersProxy = ref([]);
}, const selectedParam = ref('');
unmounted () { const modalInnerHeight = ref(400);
window.removeEventListener('resize', this.getModalInnerHeight); const i = ref(1);
},
methods: { const selectedParamObj = computed(() => {
typeClass (type) { return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
});
const isChanged = computed(() => {
return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
});
const customizations = computed(() => {
return props.workspace.customizations;
});
const typeClass = (type: string) => {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return ''; return '';
}, };
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); const confirmParametersChange = () => {
}, emit('parameters-update', parametersProxy.value);
selectParameter (event, uid) { };
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid; const selectParameter = (event: MouseEvent, uid: string) => {
}, // eslint-disable-next-line @typescript-eslint/no-explicit-any
getModalInnerHeight () { if (selectedParam.value !== uid && !(event.target as any).classList.contains('remove-field'))
selectedParam.value = uid;
};
const getModalInnerHeight = () => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
if (modalBody) if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, };
addParameter () {
const addParameter = () => {
const newUid = uidGen(); const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, { parametersProxy.value = [...parametersProxy.value, {
_antares_id: newUid, _antares_id: newUid,
name: `param${this.i++}`, name: `param${i.value++}`,
type: this.workspace.dataTypes[0].types[0].name, type: props.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: '' length: ''
}]; }];
if (this.parametersProxy.length === 1) if (parametersProxy.value.length === 1)
this.resetSelectedID(); resetSelectedID();
setTimeout(() => { setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; parametersPanel.value.scrollTop = parametersPanel.value.scrollHeight + 60;
this.selectedParam = newUid; selectedParam.value = newUid;
}, 20); }, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
}
}
}; };
const removeParameter = (uid: string) => {
parametersProxy.value = parametersProxy.value.filter(param => param._antares_id !== uid);
if (parametersProxy.value.length && selectedParam.value === uid)
resetSelectedID();
};
const clearChanges = () => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (!parametersProxy.value.some(param => param.name === selectedParam.value))
resetSelectedID();
};
const resetSelectedID = () => {
selectedParam.value = parametersProxy.value.length ? parametersProxy.value[0]._antares_id : '';
};
onMounted(() => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (parametersProxy.value.length)
resetSelectedID();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,26 +11,26 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal"> <button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" /> <i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span> <span>{{ t('word.timing') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
v-model="localScheduler.name" v-model="localScheduler.name"
class="form-input" class="form-input"
@ -50,19 +50,19 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localScheduler.definer" v-model="localScheduler.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label> <label class="form-label">{{ t('word.comment') }}</label>
<input <input
v-model="localScheduler.comment" v-model="localScheduler.comment"
class="form-input" class="form-input"
@ -72,7 +72,7 @@
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label> <label class="form-label mr-2">{{ t('word.state') }}</label>
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="localScheduler.state" v-model="localScheduler.state"
@ -103,7 +103,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label> <label class="form-label ml-2">{{ t('message.schedulerBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -122,245 +122,231 @@
/> />
</div> </div>
</template> </template>
<script setup lang="ts">
<script> import { AlterEventParams, EventInfos } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal'; import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal.vue';
import Schedulers from '@/ipc-api/Schedulers';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import Schedulers from '@/ipc-api/Schedulers';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsScheduler',
components: { const props = defineProps({
BaseLoader,
QueryEditor,
WorkspaceTabPropsSchedulerTimingModal,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
scheduler: String, scheduler: String,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
renameTabs, renameTabs,
newTab,
changeBreadcrumbs, changeBreadcrumbs,
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const isLoading = ref(false);
selectedWorkspace, const isSaving = ref(false);
getWorkspace, const isTimingModal = ref(false);
refreshStructure, const originalScheduler: Ref<EventInfos> = ref(null);
renameTabs, const localScheduler: Ref<EventInfos> = ref({} as EventInfos);
newTab, const lastScheduler = ref(null);
changeBreadcrumbs, const sqlProxy = ref('');
setUnsavedChanges const editorHeight = ref(300);
};
},
data () {
return {
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: null,
localScheduler: { sql: '' },
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const workspace = computed(() => {
}, return getWorkspace(props.connection.uid);
users () { });
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) { const isChanged = computed(() => {
const [name, host] = this.originalScheduler.definer.replaceAll('`', '').split('@'); return JSON.stringify(originalScheduler.value) !== JSON.stringify(localScheduler.value);
});
const isDefinerInUsers = computed(() => {
return originalScheduler.value ? workspace.value.users.some(user => originalScheduler.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
});
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalScheduler.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host }); users.unshift({ name, host });
} }
return users; return users;
} });
},
watch: {
async schema () {
if (this.isSelected) {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
this.lastScheduler = this.scheduler;
}
},
async scheduler () {
if (this.isSelected) {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
this.lastScheduler = this.scheduler;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.scheduler });
setTimeout(() => { const getSchedulerData = async () => {
this.resizeQueryEditor(); if (!props.scheduler) return;
}, 200);
if (this.lastScheduler !== this.scheduler) isLoading.value = true;
this.getSchedulerData(); lastScheduler.value = props.scheduler;
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getSchedulerData () {
if (!this.scheduler) return;
this.isLoading = true;
this.lastScheduler = this.scheduler;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
scheduler: this.scheduler scheduler: props.scheduler
}; };
try { try {
const { status, response } = await Schedulers.getSchedulerInformations(params); const { status, response } = await Schedulers.getSchedulerInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalScheduler = response; originalScheduler.value = response;
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler)); localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
this.sqlProxy = this.localScheduler.sql; sqlProxy.value = localScheduler.value.sql;
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
scheduler: { scheduler: {
...this.localScheduler, ...localScheduler.value,
schema: this.schema, schema: props.schema,
oldName: this.originalScheduler.name oldName: originalScheduler.value.name
} } as AlterEventParams
}; };
try { try {
const { status, response } = await Schedulers.alterScheduler(params); const { status, response } = await Schedulers.alterScheduler(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.originalScheduler.name; const oldName = originalScheduler.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localScheduler.name) { if (oldName !== localScheduler.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localScheduler.name, elementNewName: localScheduler.value.name,
elementType: 'scheduler' elementType: 'scheduler'
}); });
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name }); changeBreadcrumbs({ schema: props.schema, scheduler: localScheduler.value.name });
} }
else else
this.getSchedulerData(); getSchedulerData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler)); const clearChanges = () => {
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql); localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
}, queryEditor.value.editor.session.setValue(localScheduler.value.sql);
resizeQueryEditor () { };
if (this.$refs.queryEditor) {
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
showTimingModal () {
this.isTimingModal = true; const showTimingModal = () => {
}, isTimingModal.value = true;
hideTimingModal () { };
this.isTimingModal = false;
}, const hideTimingModal = () => {
timingUpdate (options) { isTimingModal.value = false;
this.localScheduler = options; };
},
onKey (e) { const timingUpdate = (options: EventInfos) => {
if (this.isSelected) { localScheduler.value = options;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
lastScheduler.value = props.scheduler;
}
});
watch(() => props.scheduler, async () => {
if (props.isSelected) {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
lastScheduler.value = props.scheduler;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, scheduler: props.scheduler });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastScheduler.value !== props.scheduler)
getSchedulerData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="400" size="400"
@confirm="confirmOptionsChange" @confirm="confirmOptionsChange"
@hide="$emit('hide')" @hide="$emit('hide')"
@ -8,14 +8,14 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-timer mr-1" /> <i class="mdi mdi-24px mdi-timer mr-1" />
<span class="cut-text">{{ $t('word.timing') }} "{{ localOptions.name }}"</span> <span class="cut-text">{{ t('word.timing') }} "{{ localOptions.name }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.execution') }} {{ t('word.execution') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -61,7 +61,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.starts') }} {{ t('word.starts') }}
</label> </label>
<div class="column"> <div class="column">
<div class="input-group"> <div class="input-group">
@ -82,7 +82,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.ends') }} {{ t('word.ends') }}
</label> </label>
<div class="column"> <div class="column">
<div class="input-group"> <div class="input-group">
@ -124,7 +124,7 @@
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline mt-2"> <label class="form-checkbox form-inline mt-2">
<input v-model="optionsProxy.preserve" type="checkbox"><i class="form-icon" /> {{ $t('message.preserveOnCompletion') }} <input v-model="optionsProxy.preserve" type="checkbox"><i class="form-icon" /> {{ t('message.preserveOnCompletion') }}
</label> </label>
</div> </div>
</div> </div>
@ -133,52 +133,47 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { Ref, ref } from 'vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import * as moment from 'moment';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { EventInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsSchedulerTimingModal',
components: { const props = defineProps({
ConfirmModal,
BaseSelect
},
props: {
localOptions: Object, localOptions: Object,
workspace: Object workspace: Object
}, });
emits: ['hide', 'options-update'],
data () { const emit = defineEmits(['hide', 'options-update']);
return {
optionsProxy: {}, const optionsProxy: Ref<EventInfos> = ref({} as EventInfos);
isOptionsChanging: false, const hasStart = ref(false);
hasStart: false, const hasEnd = ref(false);
hasEnd: false
const confirmOptionsChange = () => {
if (!hasStart.value) optionsProxy.value.starts = '';
if (!hasEnd.value) optionsProxy.value.ends = '';
emit('options-update', optionsProxy.value);
}; };
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
this.hasStart = !!this.optionsProxy.starts; const isNumberOrMinus = (event: KeyboardEvent) => {
this.hasEnd = !!this.optionsProxy.ends;
if (!this.optionsProxy.at) this.optionsProxy.at = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.starts) this.optionsProxy.starts = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.ends) this.optionsProxy.ends = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.every.length) this.optionsProxy.every = ['1', 'DAY'];
},
methods: {
confirmOptionsChange () {
if (!this.hasStart) this.optionsProxy.starts = '';
if (!this.hasEnd) this.optionsProxy.ends = '';
this.$emit('options-update', this.optionsProxy);
},
isNumberOrMinus (event) {
if (!/\d/.test(event.key) && event.key !== '-') if (!/\d/.test(event.key) && event.key !== '-')
return event.preventDefault(); return event.preventDefault();
}
}
}; };
optionsProxy.value = JSON.parse(JSON.stringify(props.localOptions));
hasStart.value = !!optionsProxy.value.starts;
hasEnd.value = !!optionsProxy.value.ends;
if (!optionsProxy.value.at) optionsProxy.value.at = moment().format('YYYY-MM-DD HH:mm:ss');
if (!optionsProxy.value.starts) optionsProxy.value.starts = moment().format('YYYY-MM-DD HH:mm:ss');
if (!optionsProxy.value.ends) optionsProxy.value.ends = moment().format('YYYY-MM-DD HH:mm:ss');
if (!optionsProxy.value.every.length) optionsProxy.value.every = ['1', 'DAY'];
</script> </script>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged || isSaving" :disabled="!isChanged || isSaving"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -28,20 +28,20 @@
<button <button
:disabled="isSaving" :disabled="isSaving"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="$t('message.addNewField')" :title="t('message.addNewField')"
@click="addField" @click="addField"
> >
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
:disabled="isSaving" :disabled="isSaving"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')" :title="t('message.manageIndexes')"
@click="showIntdexesModal" @click="showIntdexesModal"
> >
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span>{{ $t('word.indexes') }}</span> <span>{{ t('word.indexes') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
@ -49,11 +49,11 @@
@click="showForeignModal" @click="showForeignModal"
> >
<i class="mdi mdi-24px mdi-key-link mr-1" /> <i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span> <span>{{ t('word.foreignKeys') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -63,7 +63,7 @@
<div class="columns mb-4"> <div class="columns mb-4">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
v-model="localOptions.name" v-model="localOptions.name"
class="form-input" class="form-input"
@ -73,7 +73,7 @@
</div> </div>
<div v-if="workspace.customizations.comment" class="column"> <div v-if="workspace.customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label> <label class="form-label">{{ t('word.comment') }}</label>
<input <input
v-model="localOptions.comment" v-model="localOptions.comment"
class="form-input" class="form-input"
@ -85,7 +85,7 @@
<div v-if="workspace.customizations.autoIncrement" class="column col-auto"> <div v-if="workspace.customizations.autoIncrement" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.autoIncrement') }} {{ t('word.autoIncrement') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -99,7 +99,7 @@
<div v-if="workspace.customizations.collations" class="column col-auto"> <div v-if="workspace.customizations.collations" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.collation') }} {{ t('word.collation') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localOptions.collation" v-model="localOptions.collation"
@ -113,7 +113,7 @@
<div v-if="workspace.customizations.engines" class="column col-auto"> <div v-if="workspace.customizations.engines" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.engine') }} {{ t('word.engine') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localOptions.engine" v-model="localOptions.engine"
@ -172,35 +172,31 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onBeforeUnmount, Ref, ref, watch } from 'vue';
import { AlterTableParams, TableField, TableForeign, TableIndex, TableInfos, TableOptions } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields.vue';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal.vue';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTable',
components: { const props = defineProps({
BaseLoader,
WorkspaceTabPropsTableFields,
WorkspaceTabPropsTableIndexesModal,
WorkspaceTabPropsTableForeignModal,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
isSelected: Boolean, isSelected: Boolean,
table: String, table: String,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
@ -215,127 +211,80 @@ export default {
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const indexTable: Ref<Component & {tableWrapper: HTMLDivElement }> = ref(null);
addNotification, const firstInput: Ref<HTMLInputElement> = ref(null);
getDatabaseVariable, const isLoading = ref(false);
getWorkspace, const isSaving = ref(false);
selectedWorkspace, const isIndexesModal = ref(false);
refreshStructure, const isForeignModal = ref(false);
setUnsavedChanges, const originalFields: Ref<TableField[]> = ref([]);
renameTabs, const localFields: Ref<TableField[]> = ref([]);
changeBreadcrumbs const originalKeyUsage: Ref<TableForeign[]> = ref([]);
}; const localKeyUsage: Ref<TableForeign[]> = ref([]);
}, const originalIndexes: Ref<TableIndex[]> = ref([]);
data () { const localIndexes: Ref<TableIndex[]> = ref([]);
return { const tableOptions: Ref<TableOptions> = ref(null);
isLoading: false, const localOptions: Ref<TableOptions> = ref({} as TableOptions);
isSaving: false, const lastTable = ref(null);
isIndexesModal: false, const newFieldsCounter = ref(0);
isForeignModal: false,
isOptionsChanging: false, const workspace = computed(() => {
originalFields: [], return getWorkspace(props.connection.uid);
localFields: [], });
originalKeyUsage: [],
localKeyUsage: [], const defaultCollation = computed(() => {
originalIndexes: [], if (workspace.value.customizations.collations)
localIndexes: [], return getDatabaseVariable(selectedWorkspace.value, 'collation_server')?.value || '';
tableOptions: {},
localOptions: {},
lastTable: null,
newFieldsCounter: 0
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server')?.value || '';
return ''; return '';
}, });
defaultEngine () {
const engine = this.getDatabaseVariable(this.connection.uid, 'default_storage_engine'); const schemaTables = computed(() => {
return engine ? engine.value : ''; const schemaTables = workspace.value.structure
}, .filter(schema => schema.name === props.schema)
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables); .map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}, });
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
}
},
watch: {
schema () {
if (this.isSelected) {
this.getFieldsData();
this.lastTable = this.table;
}
},
table () {
if (this.isSelected) {
this.getFieldsData();
this.lastTable = this.table;
}
},
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, table: this.table });
if (this.lastTable !== this.table) const isChanged = computed(() => {
this.getFieldsData(); return JSON.stringify(originalFields.value) !== JSON.stringify(localFields.value) ||
} JSON.stringify(originalKeyUsage.value) !== JSON.stringify(localKeyUsage.value) ||
}, JSON.stringify(originalIndexes.value) !== JSON.stringify(localIndexes.value) ||
isChanged (val) { JSON.stringify(tableOptions.value) !== JSON.stringify(localOptions.value);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val }); });
}
},
created () {
this.getFieldsData();
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getTableOptions (params) {
const db = this.workspace.structure.find(db => db.name === this.schema);
if (db && db.tables.length && this.table) const getTableOptions = async (params: {uid: string; schema: string; table: string}) => {
this.tableOptions = db.tables.find(table => table.name === this.table); const db = workspace.value.structure.find(db => db.name === props.schema);
if (db && db.tables.length && props.table)
tableOptions.value = db.tables.find(table => table.name === props.table);
else { else {
const { status, response } = await Tables.getTableOptions(params); const { status, response } = await Tables.getTableOptions(params);
if (status === 'success') if (status === 'success')
this.tableOptions = response; tableOptions.value = response;
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
}, };
async getFieldsData () {
if (!this.table) return;
this.localFields = []; const getFieldsData = async () => {
this.lastTable = this.table; if (!props.table) return;
this.newFieldsCounter = 0;
this.isLoading = true; localFields.value = [];
lastTable.value = props.table;
newFieldsCounter.value = 0;
isLoading.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
table: this.table table: props.table
}; };
try { try {
await this.getTableOptions(params); await getTableOptions(params);
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); localOptions.value = JSON.parse(JSON.stringify(tableOptions.value));
} }
catch (err) { catch (err) {
console.error(err); console.error(err);
@ -344,7 +293,7 @@ export default {
try { // Columns data try { // Columns data
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') { if (status === 'success') {
this.originalFields = response.map(field => { originalFields.value = response.map((field: TableField) => {
if (field.autoIncrement) if (field.autoIncrement)
field.defaultType = 'autoincrement'; field.defaultType = 'autoincrement';
else if (field.default === null) else if (field.default === null)
@ -361,30 +310,31 @@ export default {
return { ...field, _antares_id: uidGen() }; return { ...field, _antares_id: uidGen() };
}); });
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); localFields.value = JSON.parse(JSON.stringify(originalFields.value));
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
try { // Indexes try { // Indexes
const { status, response } = await Tables.getTableIndexes(params); const { status, response } = await Tables.getTableIndexes(params);
if (status === 'success') { if (status === 'success') {
const indexesObj = response.reduce((acc, curr) => { const indexesObj = response.reduce((acc: {[key: string]: TableIndex[]}, curr: TableIndex) => {
acc[curr.name] = acc[curr.name] || []; acc[curr.name] = acc[curr.name] || [];
acc[curr.name].push(curr); acc[curr.name].push(curr);
return acc; return acc;
}, {}); }, {});
this.originalIndexes = Object.keys(indexesObj).map(index => { originalIndexes.value = Object.keys(indexesObj).map(index => {
return { return {
_antares_id: uidGen(), _antares_id: uidGen(),
name: index, name: index,
fields: indexesObj[index].map(field => field.column), // eslint-disable-next-line @typescript-eslint/no-explicit-any
fields: indexesObj[index].map((field: any) => field.column),
type: indexesObj[index][0].type, type: indexesObj[index][0].type,
comment: indexesObj[index][0].comment, comment: indexesObj[index][0].comment,
indexType: indexesObj[index][0].indexType, indexType: indexesObj[index][0].indexType,
@ -393,91 +343,93 @@ export default {
}; };
}); });
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes)); localIndexes.value = JSON.parse(JSON.stringify(originalIndexes.value));
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
try { // Key usage (foreign keys) try { // Key usage (foreign keys)
const { status, response } = await Tables.getKeyUsage(params); const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') { if (status === 'success') {
this.originalKeyUsage = response.map(foreign => { originalKeyUsage.value = response.map((foreign: TableForeign) => {
return { return {
_antares_id: uidGen(), _antares_id: uidGen(),
...foreign ...foreign
}; };
}); });
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage)); localKeyUsage.value = JSON.parse(JSON.stringify(originalKeyUsage.value));
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
// FIELDS // FIELDS
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._antares_id], []); const originalIDs = originalFields.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._antares_id], []); const localIDs = localFields.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Fields Additions // Fields Additions
const additions = this.localFields.filter(field => !originalIDs.includes(field._antares_id)).map(field => { const additions = localFields.value.filter(field => !originalIDs.includes(field._antares_id)).map(field => {
const lI = this.localFields.findIndex(localField => localField._antares_id === field._antares_id); const lI = localFields.value.findIndex(localField => localField._antares_id === field._antares_id);
const after = lI > 0 ? this.localFields[lI - 1].name : false; const after = lI > 0 ? localFields.value[lI - 1].name : false;
return { ...field, after }; return { ...field, after };
}); });
// Fields Deletions // Fields Deletions
const deletions = this.originalFields.filter(field => !localIDs.includes(field._antares_id)); const deletions = originalFields.value.filter(field => !localIDs.includes(field._antares_id));
// Fields Changes // Fields Changes
const changes = []; const changes: TableField[] & {after: string | boolean; orgName: string}[] = [];
this.localFields.forEach((field, i) => { localFields.value.forEach((field, i) => {
const originalField = this.originalFields.find(oField => oField._antares_id === field._antares_id); const originalField = originalFields.value.find(oField => oField._antares_id === field._antares_id);
if (!originalField) return; if (!originalField) return;
const after = i > 0 ? this.localFields[i - 1].name : false; const after = i > 0 ? localFields.value[i - 1].name : false;
const orgName = originalField.name; const orgName = originalField.name;
changes.push({ ...field, after, orgName }); changes.push({ ...field, after, orgName });
}); });
// OPTIONS // OPTIONS
const options = Object.keys(this.localOptions).reduce((acc, option) => { const options = Object.keys(localOptions.value).reduce((acc: {[key:string]: TableInfos}, option: keyof TableInfos) => {
if (this.localOptions[option] !== this.tableOptions[option]) if (localOptions.value[option] !== tableOptions.value[option])
acc[option] = this.localOptions[option]; // eslint-disable-next-line @typescript-eslint/no-explicit-any
acc[option] = localOptions.value[option] as any;
return acc; return acc;
}, {}); }, {});
// INDEXES // INDEXES
const indexChanges = { const indexChanges = {
additions: [], additions: [] as TableIndex[],
changes: [], changes: [] as TableIndex[],
deletions: [] deletions: [] as TableIndex[]
}; };
const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._antares_id], []); const originalIndexIDs = originalIndexes.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._antares_id], []); const localIndexIDs = localIndexes.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Index Additions // Index Additions
indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._antares_id)); indexChanges.additions = localIndexes.value.filter(index => !originalIndexIDs.includes(index._antares_id));
// Index Changes // Index Changes
this.originalIndexes.forEach(originalIndex => { originalIndexes.value.forEach(originalIndex => {
const lI = this.localIndexes.findIndex(localIndex => localIndex._antares_id === originalIndex._antares_id); const lI = localIndexes.value.findIndex(localIndex => localIndex._antares_id === originalIndex._antares_id);
if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) { if (JSON.stringify(originalIndex) !== JSON.stringify(localIndexes.value[lI])) {
if (this.localIndexes[lI]) { if (localIndexes.value[lI]) {
indexChanges.changes.push({ indexChanges.changes.push({
...this.localIndexes[lI], ...localIndexes.value[lI],
oldName: originalIndex.name, oldName: originalIndex.name,
oldType: originalIndex.type oldType: originalIndex.type
}); });
@ -486,27 +438,27 @@ export default {
}); });
// Index Deletions // Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._antares_id)); indexChanges.deletions = originalIndexes.value.filter(index => !localIndexIDs.includes(index._antares_id));
// FOREIGN KEYS // FOREIGN KEYS
const foreignChanges = { const foreignChanges = {
additions: [], additions: [] as TableForeign[],
changes: [], changes: [] as TableForeign[],
deletions: [] deletions: [] as TableForeign[]
}; };
const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._antares_id], []); const originalForeignIDs = originalKeyUsage.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._antares_id], []); const localForeignIDs = localKeyUsage.value.reduce((acc, curr) => [...acc, curr._antares_id], []);
// Foreigns Additions // Foreigns Additions
foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._antares_id)); foreignChanges.additions = localKeyUsage.value.filter(foreign => !originalForeignIDs.includes(foreign._antares_id));
// Foreigns Changes // Foreigns Changes
this.originalKeyUsage.forEach(originalForeign => { originalKeyUsage.value.forEach(originalForeign => {
const lI = this.localKeyUsage.findIndex(localForeign => localForeign._antares_id === originalForeign._antares_id); const lI = localKeyUsage.value.findIndex(localForeign => localForeign._antares_id === originalForeign._antares_id);
if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) { if (JSON.stringify(originalForeign) !== JSON.stringify(localKeyUsage.value[lI])) {
if (this.localKeyUsage[lI]) { if (localKeyUsage.value[lI]) {
foreignChanges.changes.push({ foreignChanges.changes.push({
...this.localKeyUsage[lI], ...localKeyUsage.value[lI],
oldName: originalForeign.constraintName oldName: originalForeign.constraintName
}); });
} }
@ -514,18 +466,18 @@ export default {
}); });
// Foreigns Deletions // Foreigns Deletions
foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._antares_id)); foreignChanges.deletions = originalKeyUsage.value.filter(foreign => !localForeignIDs.includes(foreign._antares_id));
// ALTER // ALTER
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
table: this.table, table: props.table,
tableStructure: { tableStructure: {
name: this.localOptions.name, name: localOptions.value.name,
fields: this.localFields, fields: localFields.value,
foreigns: this.localKeyUsage, foreigns: localKeyUsage.value,
indexes: this.localIndexes indexes: localIndexes.value
}, },
additions, additions,
changes, changes,
@ -533,115 +485,122 @@ export default {
indexChanges, indexChanges,
foreignChanges, foreignChanges,
options options
}; } as unknown as AlterTableParams;
try { try {
const { status, response } = await Tables.alterTable(params); const { status, response } = await Tables.alterTable(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.tableOptions.name; const oldName = tableOptions.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localOptions.name) { if (oldName !== localOptions.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localOptions.name, elementNewName: localOptions.value.name,
elementType: 'table' elementType: 'table'
}); });
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name }); changeBreadcrumbs({ schema: props.schema, table: localOptions.value.name });
} }
else else
this.getFieldsData(); getFieldsData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
this.newFieldsCounter = 0; newFieldsCounter.value = 0;
}, };
clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); const clearChanges = () => {
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes)); localFields.value = JSON.parse(JSON.stringify(originalFields.value));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage)); localIndexes.value = JSON.parse(JSON.stringify(originalIndexes.value));
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); localKeyUsage.value = JSON.parse(JSON.stringify(originalKeyUsage.value));
this.newFieldsCounter = 0; localOptions.value = JSON.parse(JSON.stringify(tableOptions.value));
}, newFieldsCounter.value = 0;
addField () { };
this.localFields.push({
const addField = () => {
localFields.value.push({
_antares_id: uidGen(), _antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${t('word.field', 1)}_${++newFieldsCounter.value}`,
key: '', key: '',
type: this.workspace.dataTypes[0].types[0].name, // eslint-disable-next-line @typescript-eslint/no-explicit-any
schema: this.schema, type: (workspace.value.dataTypes[0] as any).types[0].name,
table: this.table, schema: props.schema,
numPrecision: null, numPrecision: null,
numLength: this.workspace.dataTypes[0].types[0].length, // eslint-disable-next-line @typescript-eslint/no-explicit-any
numLength: (workspace.value.dataTypes[0] as any).types[0].length,
datePrecision: null, datePrecision: null,
charLength: null, charLength: null,
nullable: false, nullable: false,
unsigned: false, unsigned: false,
zerofill: false, zerofill: false,
order: this.localFields.length + 1, order: localFields.value.length + 1,
default: null, default: null,
charset: null, charset: null,
collation: this.defaultCollation, collation: defaultCollation.value,
autoIncrement: false, autoIncrement: false,
onUpdate: '', onUpdate: '',
comment: '' comment: ''
}); });
setTimeout(() => { setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper; const scrollable = indexTable.value.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 20);
}, };
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => { const renameField = (payload: {index: string; new: string; old: string}) => {
localIndexes.value = localIndexes.value.map(index => {
const fi = index.fields.findIndex(field => field === payload.old); const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1) if (fi !== -1)
index.fields[fi] = payload.new; index.fields[fi] = payload.new;
return index; return index;
}); });
this.localKeyUsage = this.localKeyUsage.map(key => { localKeyUsage.value = localKeyUsage.value.map(key => {
if (key.field === payload.old) if (key.field === payload.old)
key.field = payload.new; key.field = payload.new;
return key; return key;
}); });
}, };
duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid)); const duplicateField = (uid: string) => {
const fieldToClone = Object.assign({}, localFields.value.find(field => field._antares_id === uid));
fieldToClone._antares_id = uidGen(); fieldToClone._antares_id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`; fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1; fieldToClone.order = localFields.value.length + 1;
this.localFields = [...this.localFields, fieldToClone]; localFields.value = [...localFields.value, fieldToClone];
setTimeout(() => { setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper; const scrollable = indexTable.value.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30; scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20); }, 20);
}, };
removeField (uid) {
this.localFields = this.localFields.filter(field => field._antares_id !== uid); const removeField = (uid: string) => {
this.localKeyUsage = this.localKeyUsage.filter(fk =>// Clear foreign keys localFields.value = localFields.value.filter(field => field._antares_id !== uid);
this.localFields.some(field => field.name === fk.field) localKeyUsage.value = localKeyUsage.value.filter(fk =>// Clear foreign keys
localFields.value.some(field => field.name === fk.field)
); );
this.localIndexes = this.localIndexes.filter(index =>// Clear indexes localIndexes.value = localIndexes.value.filter(index =>// Clear indexes
this.localFields.some(field => localFields.value.some(field =>
index.fields.includes(field.name) index.fields.includes(field.name)
) )
); );
}, };
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, { const addNewIndex = (payload: { index: string; field: string }) => {
localIndexes.value = [...localIndexes.value, {
_antares_id: uidGen(), _antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field, name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field], fields: [payload.field],
@ -651,43 +610,80 @@ export default {
indexComment: '', indexComment: '',
cardinality: 0 cardinality: 0
}]; }];
}, };
addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => { const addToIndex = (payload: { index: string; field: string }) => {
localIndexes.value = localIndexes.value.map(index => {
if (index._antares_id === payload.index) index.fields.push(payload.field); if (index._antares_id === payload.index) index.fields.push(payload.field);
return index; return index;
}); });
}, };
optionsUpdate (options) {
this.localOptions = options; const showIntdexesModal = () => {
}, isIndexesModal.value = true;
showIntdexesModal () { };
this.isIndexesModal = true;
}, const hideIndexesModal = () => {
hideIndexesModal () { isIndexesModal.value = false;
this.isIndexesModal = false; };
},
indexesUpdate (indexes) { const indexesUpdate = (indexes: TableIndex[]) => {
this.localIndexes = indexes; localIndexes.value = indexes;
}, };
showForeignModal () {
this.isForeignModal = true; const showForeignModal = () => {
}, isForeignModal.value = true;
hideForeignModal () { };
this.isForeignModal = false;
}, const hideForeignModal = () => {
foreignsUpdate (foreigns) { isForeignModal.value = false;
this.localKeyUsage = foreigns; };
},
onKey (e) { const foreignsUpdate = (foreigns: TableForeign[]) => {
if (this.isSelected) { localKeyUsage.value = foreigns;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, () => {
if (props.isSelected) {
getFieldsData();
lastTable.value = props.table;
}
});
watch(() => props.table, () => {
if (props.isSelected) {
getFieldsData();
lastTable.value = props.table;
}
});
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, table: props.table });
if (lastTable.value !== props.table)
getFieldsData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
getFieldsData();
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -4,7 +4,7 @@
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element"> <div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-plus text-light pr-1" /> {{ $t('message.createNewIndex') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-key-plus text-light pr-1" /> {{ t('message.createNewIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div <div
@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<div v-if="indexes.length" class="context-element"> <div v-if="indexes.length" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-arrow-right text-light pr-1" /> {{ $t('message.addToIndex') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-key-arrow-right text-light pr-1" /> {{ t('message.addToIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div <div
@ -34,54 +34,54 @@
</div> </div>
</div> </div>
<div class="context-element" @click="duplicateField"> <div class="context-element" @click="duplicateField">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ t('word.duplicate') }}</span>
</div> </div>
<div class="context-element" @click="deleteField"> <div class="context-element" @click="deleteField">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ t('message.deleteField') }}</span>
</div> </div>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import BaseContextMenu from '@/components/BaseContextMenu'; import { computed, Prop } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import { TableIndex } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabQueryTableContext',
components: { const props = defineProps({
BaseContextMenu
},
props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
indexes: Array, indexes: Array as Prop<TableIndex[]>,
indexTypes: Array, indexTypes: Array as Prop<string[]>,
selectedField: Object selectedField: Object
}, });
emits: ['close-context', 'duplicate-selected', 'delete-selected', 'add-new-index', 'add-to-index'],
computed: { const emit = defineEmits(['close-context', 'duplicate-selected', 'delete-selected', 'add-new-index', 'add-to-index']);
hasPrimary () {
return this.indexes.some(index => index.type === 'PRIMARY'); const hasPrimary = computed(() => props.indexes.some(index => index.type === 'PRIMARY'));
}
}, const closeContext = () => {
methods: { emit('close-context');
closeContext () { };
this.$emit('close-context');
}, const duplicateField = () => {
duplicateField () { emit('duplicate-selected');
this.$emit('duplicate-selected'); closeContext();
this.closeContext(); };
},
deleteField () { const deleteField = () => {
this.$emit('delete-selected'); emit('delete-selected');
this.closeContext(); closeContext();
}, };
addNewIndex (index) {
this.$emit('add-new-index', { field: this.selectedField.name, index }); const addNewIndex = (index: string) => {
this.closeContext(); emit('add-new-index', { field: props.selectedField.name, index });
}, closeContext();
addToIndex (index) { };
this.$emit('add-to-index', { field: this.selectedField.name, index });
this.closeContext(); const addToIndex = (index: string) => {
} emit('add-to-index', { field: props.selectedField.name, index });
} closeContext();
}; };
</script> </script>

View File

@ -13,89 +13,89 @@
@delete-selected="removeField" @delete-selected="removeField"
@duplicate-selected="duplicateField" @duplicate-selected="duplicateField"
@close-context="isContext = false" @close-context="isContext = false"
@add-new-index="$emit('add-new-index', $event)" @add-new-index="emit('add-new-index', $event)"
@add-to-index="$emit('add-to-index', $event)" @add-to-index="emit('add-to-index', $event)"
/> />
<div ref="propTable" class="table table-hover"> <div ref="propTable" class="table table-hover">
<div class="thead"> <div class="thead">
<div class="tr"> <div class="tr">
<div class="th"> <div class="th">
<div class="text-right"> <div class="text-right">
{{ $t('word.order') }} {{ t('word.order') }}
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="table-column-title"> <div class="table-column-title">
{{ $tc('word.key', 2) }} {{ t('word.key', 2) }}
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable min-100"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.name') }} {{ t('word.name') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable min-100"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.type') }} {{ t('word.type') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.tableArray" class="th"> <div v-if="customizations.tableArray" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.array') }} {{ t('word.array') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.length') }} {{ t('word.length') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.unsigned" class="th"> <div v-if="customizations.unsigned" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.unsigned') }} {{ t('word.unsigned') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.nullable" class="th"> <div v-if="customizations.nullable" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.allowNull') }} {{ t('message.allowNull') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.zerofill" class="th"> <div v-if="customizations.zerofill" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.zeroFill') }} {{ t('message.zeroFill') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.default') }} {{ t('word.default') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.comment" class="th"> <div v-if="customizations.comment" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.comment') }} {{ t('word.comment') }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.collation" class="th"> <div v-if="customizations.collation" class="th">
<div class="column-resizable min-100"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.collation') }} {{ t('word.collation') }}
</div> </div>
</div> </div>
</div> </div>
@ -116,7 +116,7 @@
:data-types="dataTypes" :data-types="dataTypes"
:customizations="customizations" :customizations="customizations"
@contextmenu="contextMenu" @contextmenu="contextMenu"
@rename-field="$emit('rename-field', $event)" @rename-field="emit('rename-field', $event)"
/> />
</template> </template>
</Draggable> </Draggable>
@ -124,135 +124,115 @@
</div> </div>
</template> </template>
<script>// TODO: expose tableWrapper <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onMounted, onUnmounted, onUpdated, Prop, ref, Ref, watch } from 'vue';
import { useNotificationsStore } from '@/stores/notifications'; import { useI18n } from 'vue-i18n';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Draggable from 'vuedraggable'; import * as Draggable from 'vuedraggable';
import TableRow from '@/components/WorkspaceTabPropsTableRow'; import TableRow from '@/components/WorkspaceTabPropsTableRow.vue';
import TableContext from '@/components/WorkspaceTabPropsTableContext'; import TableContext from '@/components/WorkspaceTabPropsTableContext.vue';
import { TableField, TableForeign, TableIndex } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTableFields',
components: { const props = defineProps({
TableRow, fields: Array as Prop<TableField[]>,
TableContext, indexes: Array as Prop<TableIndex[]>,
Draggable foreigns: Array as Prop<TableForeign[]>,
}, indexTypes: Array as Prop<string[]>,
props: {
fields: Array,
indexes: Array,
foreigns: Array,
indexTypes: Array,
tabUid: [String, Number], tabUid: [String, Number],
connUid: String, connUid: String,
table: String, table: String,
schema: String, schema: String,
mode: String mode: String
}, });
emits: ['add-new-index', 'add-to-index', 'rename-field', 'duplicate-field', 'remove-field'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['add-new-index', 'add-to-index', 'rename-field', 'duplicate-field', 'remove-field']);
const workspacesStore = useWorkspacesStore();
const { getWorkspace } = workspacesStore; const { getWorkspace } = workspacesStore;
return { const tableWrapper: Ref<HTMLDivElement> = ref(null);
addNotification, const propTable: Ref<HTMLDivElement> = ref(null);
selectedWorkspace, const resultTable: Ref<Component> = ref(null);
getWorkspace const resultsSize = ref(1000);
}; const isContext = ref(false);
}, const contextEvent = ref(null);
data () { const selectedField = ref(null);
return { const scrollElement = ref(null);
resultsSize: 1000,
isContext: false,
contextEvent: null,
selectedField: null,
scrollElement: null
};
},
computed: {
workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
customizations () {
return this.getWorkspace(this.connUid).customizations;
},
dataTypes () {
return this.getWorkspace(this.connUid).dataTypes;
},
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
tabProperties () {
return this.getWorkspaceTab(this.tabUid);
},
fieldsLength () {
return this.fields.length;
}
},
watch: {
fieldsLength () {
this.refreshScroller();
}
},
updated () {
if (this.$refs.propTable)
this.refreshScroller();
if (this.$refs.tableWrapper) const customizations = computed(() => getWorkspace(props.connUid).customizations);
this.scrollElement = this.$refs.tableWrapper; // eslint-disable-next-line @typescript-eslint/no-explicit-any
}, const dataTypes = computed(() => getWorkspace(props.connUid).dataTypes) as any;
mounted () { const fieldsLength = computed(() => props.fields.length);
window.addEventListener('resize', this.resizeResults);
}, const resizeResults = () => {
unmounted () { if (resultTable.value) {
window.removeEventListener('resize', this.resizeResults); const el = tableWrapper.value;
},
methods: {
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper;
if (el) { if (el) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; resultsSize.value = size;
} }
} }
}, };
refreshScroller () {
this.resizeResults(); const refreshScroller = () => {
}, resizeResults();
contextMenu (event, uid) { };
this.selectedField = this.fields.find(field => field._antares_id === uid);
this.contextEvent = event; const contextMenu = (event: MouseEvent, uid: string) => {
this.isContext = true; selectedField.value = props.fields.find(field => field._antares_id === uid);
}, contextEvent.value = event;
duplicateField () { isContext.value = true;
this.$emit('duplicate-field', this.selectedField._antares_id); };
},
removeField () { const duplicateField = () => {
this.$emit('remove-field', this.selectedField._antares_id); emit('duplicate-field', selectedField.value._antares_id);
}, };
getIndexes (field) {
return this.indexes.reduce((acc, curr) => { const removeField = () => {
emit('remove-field', selectedField.value._antares_id);
};
const getIndexes = (field: string) => {
return props.indexes.reduce((acc, curr) => {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type }))); acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc; return acc;
}, []).filter(f => f.name === field); }, []).filter(f => f.name === field);
}, };
getForeigns (field) {
return this.foreigns.reduce((acc, curr) => { const getForeigns = (field: string) => {
return props.foreigns.reduce((acc, curr) => {
if (curr.field === field) if (curr.field === field)
acc.push(`${curr.refTable}.${curr.refField}`); acc.push(`${curr.refTable}.${curr.refField}`);
return acc; return acc;
}, []); }, []);
}
}
}; };
watch(fieldsLength, () => {
refreshScroller();
});
onUpdated(() => {
if (propTable.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
window.addEventListener('resize', resizeResults);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeResults);
});
defineExpose({ tableWrapper });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmForeignsChange" @confirm="confirmForeignsChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> <i class="mdi mdi-24px mdi-key-link mr-1" />
<span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span> <span class="cut-text">{{ t('word.foreignKeys') }} "{{ table }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addForeign"> <button class="btn btn-dark btn-sm d-flex" @click="addForeign">
<i class="mdi mdi-24px mdi-link-plus mr-1" /> <i class="mdi mdi-24px mdi-link-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -67,7 +67,7 @@
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeIndex(foreign._antares_id)" @click.prevent="removeIndex(foreign._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -86,7 +86,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -98,7 +98,7 @@
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $tc('word.field', 1) }} {{ t('word.field', 1) }}
</label> </label>
<div class="fields-list column pt-1"> <div class="fields-list column pt-1">
<label <label
@ -114,7 +114,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3 pt-0"> <label class="form-label col-3 pt-0">
{{ $t('message.referenceTable') }} {{ t('message.referenceTable') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -129,7 +129,7 @@
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('message.referenceField') }} {{ t('message.referenceField') }}
</label> </label>
<div class="fields-list column pt-1"> <div class="fields-list column pt-1">
<label <label
@ -145,7 +145,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('message.onUpdate') }} {{ t('message.onUpdate') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -157,7 +157,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('message.onDelete') }} {{ t('message.onDelete') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -174,11 +174,11 @@
<i class="mdi mdi-key-link mdi-48px" /> <i class="mdi mdi-key-link mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoForeign') }} {{ t('message.thereAreNoForeign') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addForeign"> <button class="btn btn-primary" @click="addForeign">
{{ $t('message.createNewForeign') }} {{ t('message.createNewForeign') }}
</button> </button>
</div> </div>
</div> </div>
@ -188,176 +188,173 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Prop, Ref, ref } from 'vue';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useI18n } from 'vue-i18n';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { TableField } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTableForeignModal',
components: { const props = defineProps({
ConfirmModal,
BaseSelect
},
props: {
localKeyUsage: Array, localKeyUsage: Array,
connection: Object, connection: Object,
table: String, table: String,
schema: String, schema: String,
schemaTables: Array, schemaTables: Array,
fields: Array, fields: Array as Prop<TableField[]>,
workspace: Object workspace: Object
}, });
emits: ['foreigns-update', 'hide'],
setup () { const emit = defineEmits(['foreigns-update', 'hide']);
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
return { addNotification }; const indexesPanel: Ref<HTMLDivElement> = ref(null);
}, const foreignProxy = ref([]);
data () { const selectedForeignID = ref('');
return { const modalInnerHeight = ref(400);
foreignProxy: [], const refFields = ref({} as {[key: string]: TableField[]});
isOptionsChanging: false, const foreignActions = [
selectedForeignID: '',
modalInnerHeight: 400,
refFields: {},
foreignActions: [
'RESTRICT', 'RESTRICT',
'CASCADE', 'CASCADE',
'SET NULL', 'SET NULL',
'NO ACTION' 'NO ACTION'
] ];
};
},
computed: {
selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._antares_id === this.selectedForeignID);
},
isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
},
hasPrimary () {
return this.foreignProxy.some(foreign => foreign.type === 'PRIMARY');
}
},
mounted () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (this.foreignProxy.length) const selectedForeignObj = computed(() => foreignProxy.value.find(foreign => foreign._antares_id === selectedForeignID.value));
this.resetSelectedID(); const isChanged = computed(() => JSON.stringify(props.localKeyUsage) !== JSON.stringify(foreignProxy.value));
if (this.selectedForeignObj) const confirmForeignsChange = () => {
this.getRefFields(); foreignProxy.value = foreignProxy.value.filter(foreign =>
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
unmounted () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmForeignsChange () {
this.foreignProxy = this.foreignProxy.filter(foreign =>
foreign.field && foreign.field &&
foreign.refField && foreign.refField &&
foreign.table && foreign.table &&
foreign.refTable foreign.refTable
); );
this.$emit('foreigns-update', this.foreignProxy); emit('foreigns-update', foreignProxy.value);
}, };
selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) { const selectForeign = (event: MouseEvent, id: string) => {
this.selectedForeignID = id; // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.getRefFields(); if (selectedForeignID.value !== id && !(event.target as any).classList.contains('remove-field')) {
selectedForeignID.value = id;
getRefFields();
} }
}, };
getModalInnerHeight () {
const getModalInnerHeight = () => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
if (modalBody) if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, };
addForeign () {
this.foreignProxy = [...this.foreignProxy, { const addForeign = () => {
foreignProxy.value = [...foreignProxy.value, {
_antares_id: uidGen(), _antares_id: uidGen(),
constraintName: `FK_${uidGen()}`, constraintName: `FK_${uidGen()}`,
refSchema: this.schema, refSchema: props.schema,
table: this.table, table: props.table,
refTable: '', refTable: '',
field: '', field: '',
refField: '', refField: '',
onUpdate: this.foreignActions[0], onUpdate: foreignActions[0],
onDelete: this.foreignActions[0] onDelete: foreignActions[0]
}]; }];
if (this.foreignProxy.length === 1) if (foreignProxy.value.length === 1)
this.resetSelectedID(); resetSelectedID();
setTimeout(() => { setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60; indexesPanel.value.scrollTop = indexesPanel.value.scrollHeight + 60;
}, 20); }, 20);
}, };
removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._antares_id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length) const removeIndex = (id: string) => {
this.resetSelectedID(); foreignProxy.value = foreignProxy.value.filter(foreign => foreign._antares_id !== id);
},
clearChanges () { if (selectedForeignID.value === id && foreignProxy.value.length)
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage)); resetSelectedID();
if (!this.foreignProxy.some(foreign => foreign._antares_id === this.selectedForeignID)) };
this.resetSelectedID();
}, const clearChanges = () => {
toggleField (field) { foreignProxy.value = JSON.parse(JSON.stringify(props.localKeyUsage));
this.foreignProxy = this.foreignProxy.map(foreign => { if (!foreignProxy.value.some(foreign => foreign._antares_id === selectedForeignID.value))
if (foreign._antares_id === this.selectedForeignID) resetSelectedID();
};
const toggleField = (field: string) => {
foreignProxy.value = foreignProxy.value.map(foreign => {
if (foreign._antares_id === selectedForeignID.value)
foreign.field = field; foreign.field = field;
return foreign; return foreign;
}); });
}, };
toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => { const toggleRefField = (field: string) => {
if (foreign._antares_id === this.selectedForeignID) foreignProxy.value = foreignProxy.value.map(foreign => {
if (foreign._antares_id === selectedForeignID.value)
foreign.refField = field; foreign.refField = field;
return foreign; return foreign;
}); });
}, };
resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._antares_id : ''; const resetSelectedID = () => {
}, selectedForeignID.value = foreignProxy.value.length ? foreignProxy.value[0]._antares_id : '';
async getRefFields () { };
if (!this.selectedForeignObj.refTable) return;
const getRefFields = async () => {
if (!selectedForeignObj.value.refTable) return;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.selectedForeignObj.refSchema, schema: selectedForeignObj.value.refSchema,
table: this.selectedForeignObj.refTable table: selectedForeignObj.value.refTable
}; };
try { // Field data try { // Field data
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') { if (status === 'success') {
this.refFields = { refFields.value = {
...this.refFields, ...refFields.value,
[this.selectedForeignID]: response [selectedForeignID.value]: response
}; };
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
}
},
reloadRefFields () {
this.selectedForeignObj.refField = '';
this.getRefFields();
}
} }
}; };
const reloadRefFields = () => {
selectedForeignObj.value.refField = '';
getRefFields();
};
onMounted(() => {
foreignProxy.value = JSON.parse(JSON.stringify(props.localKeyUsage));
if (foreignProxy.value.length)
resetSelectedID();
if (selectedForeignObj.value)
getRefFields();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -384,7 +381,7 @@ export default {
} }
.fields-list { .fields-list {
max-height: 80px; max-height: 90px;
overflow: auto; overflow: auto;
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmIndexesChange" @confirm="confirmIndexesChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span> <span class="cut-text">{{ t('word.indexes') }} "{{ table }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addIndex"> <button class="btn btn-dark btn-sm d-flex" @click="addIndex">
<i class="mdi mdi-24px mdi-key-plus mr-1" /> <i class="mdi mdi-24px mdi-key-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -50,12 +50,12 @@
<div class="tile-title"> <div class="tile-title">
{{ index.name }} {{ index.name }}
</div> </div>
<small class="tile-subtitle text-gray">{{ index.type }} · {{ index.fields.length }} {{ $tc('word.field', index.fields.length) }}</small> <small class="tile-subtitle text-gray">{{ index.type }} · {{ index.fields.length }} {{ t('word.field', index.fields.length) }}</small>
</div> </div>
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeIndex(index._antares_id)" @click.prevent="removeIndex(index._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -74,7 +74,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -86,20 +86,20 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.type') }} {{ t('word.type') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
v-model="selectedIndexObj.type" v-model="selectedIndexObj.type"
:options="indexTypes" :options="indexTypes"
:option-disabled="(opt) => opt === 'PRIMARY'" :option-disabled="(opt: any) => opt === 'PRIMARY'"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $tc('word.field', fields.length) }} {{ t('word.field', fields.length) }}
</label> </label>
<div class="fields-list column pt-1"> <div class="fields-list column pt-1">
<label <label
@ -108,7 +108,7 @@
class="form-checkbox m-0" class="form-checkbox m-0"
@click.prevent="toggleField(field.name)" @click.prevent="toggleField(field.name)"
> >
<input type="checkbox" :checked="selectedIndexObj.fields.some(f => f === field.name)"> <input type="checkbox" :checked="selectedIndexObj.fields.some((f: string) => f === field.name)">
<i class="form-icon" /> {{ field.name }} <i class="form-icon" /> {{ field.name }}
</label> </label>
</div> </div>
@ -119,11 +119,11 @@
<i class="mdi mdi-key-outline mdi-48px" /> <i class="mdi mdi-key-outline mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoIndexes') }} {{ t('message.thereAreNoIndexes') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addIndex"> <button class="btn btn-primary" @click="addIndex">
{{ $t('message.createNewIndex') }} {{ t('message.createNewIndex') }}
</button> </button>
</div> </div>
</div> </div>
@ -133,72 +133,53 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Prop, Ref, ref } from 'vue';
import { TableField } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTableIndexesModal',
components: { const props = defineProps({
ConfirmModal,
BaseSelect
},
props: {
localIndexes: Array, localIndexes: Array,
table: String, table: String,
fields: Array, fields: Array as Prop<TableField[]>,
workspace: Object, workspace: Object,
indexTypes: Array indexTypes: Array
}, });
emits: ['hide', 'indexes-update'],
data () { const emit = defineEmits(['hide', 'indexes-update']);
return {
indexesProxy: [], const indexesPanel: Ref<HTMLDivElement> = ref(null);
isOptionsChanging: false, const indexesProxy = ref([]);
selectedIndexID: '', const selectedIndexID = ref('');
modalInnerHeight: 400 const modalInnerHeight = ref(400);
const selectedIndexObj = computed(() => indexesProxy.value.find(index => index._antares_id === selectedIndexID.value));
const isChanged = computed(() => JSON.stringify(props.localIndexes) !== JSON.stringify(indexesProxy.value));
const confirmIndexesChange = () => {
indexesProxy.value = indexesProxy.value.filter(index => index.fields.length);
emit('indexes-update', indexesProxy.value);
}; };
},
computed: {
selectedIndexObj () {
return this.indexesProxy.find(index => index._antares_id === this.selectedIndexID);
},
isChanged () {
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
},
hasPrimary () {
return this.indexesProxy.some(index => index.type === 'PRIMARY');
}
},
mounted () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (this.indexesProxy.length) const selectIndex = (event: MouseEvent, id: string) => {
this.resetSelectedID(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
if (selectedIndexID.value !== id && !(event.target as any).classList.contains('remove-field'))
selectedIndexID.value = id;
};
this.getModalInnerHeight(); const getModalInnerHeight = () => {
window.addEventListener('resize', this.getModalInnerHeight);
},
unmounted () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
this.indexesProxy = this.indexesProxy.filter(index => index.fields.length);
this.$emit('indexes-update', this.indexesProxy);
},
selectIndex (event, id) {
if (this.selectedIndexID !== id && !event.target.classList.contains('remove-field'))
this.selectedIndexID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
if (modalBody) if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom)); modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
}, };
addIndex () {
this.indexesProxy = [...this.indexesProxy, { const addIndex = () => {
indexesProxy.value = [...indexesProxy.value, {
_antares_id: uidGen(), _antares_id: uidGen(),
name: 'NEW_INDEX', name: 'NEW_INDEX',
fields: [], fields: [],
@ -209,40 +190,56 @@ export default {
cardinality: 0 cardinality: 0
}]; }];
if (this.indexesProxy.length === 1) if (indexesProxy.value.length === 1)
this.resetSelectedID(); resetSelectedID();
setTimeout(() => { setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60; indexesPanel.value.scrollTop = indexesPanel.value.scrollHeight + 60;
}, 20); }, 20);
}, };
removeIndex (id) {
this.indexesProxy = this.indexesProxy.filter(index => index._antares_id !== id);
if (this.selectedIndexID === id && this.indexesProxy.length) const removeIndex = (id: string) => {
this.resetSelectedID(); indexesProxy.value = indexesProxy.value.filter(index => index._antares_id !== id);
},
clearChanges () { if (selectedIndexID.value === id && indexesProxy.value.length)
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes)); resetSelectedID();
if (!this.indexesProxy.some(index => index._antares_id === this.selectedIndexID)) };
this.resetSelectedID();
}, const clearChanges = () => {
toggleField (field) { indexesProxy.value = JSON.parse(JSON.stringify(props.localIndexes));
this.indexesProxy = this.indexesProxy.map(index => { if (!indexesProxy.value.some(index => index._antares_id === selectedIndexID.value))
if (index._antares_id === this.selectedIndexID) { resetSelectedID();
};
const toggleField = (field: string) => {
indexesProxy.value = indexesProxy.value.map(index => {
if (index._antares_id === selectedIndexID.value) {
if (index.fields.includes(field)) if (index.fields.includes(field))
index.fields = index.fields.filter(f => f !== field); index.fields = index.fields.filter((f: string) => f !== field);
else else
index.fields.push(field); index.fields.push(field);
} }
return index; return index;
}); });
},
resetSelectedID () {
this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._antares_id : '';
}
}
}; };
const resetSelectedID = () => {
selectedIndexID.value = indexesProxy.value.length ? indexesProxy.value[0]._antares_id : '';
};
onMounted(() => {
indexesProxy.value = JSON.parse(JSON.stringify(props.localIndexes));
if (indexesProxy.value.length)
resetSelectedID();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -219,7 +219,7 @@
</div> </div>
<ConfirmModal <ConfirmModal
v-if="isDefaultModal" v-if="isDefaultModal"
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="400" size="400"
@confirm="editOFF" @confirm="editOFF"
@hide="hideDefaultModal" @hide="hideDefaultModal"
@ -227,7 +227,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span> <span class="cut-text">{{ t('word.default') }} "{{ row.name }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -250,7 +250,7 @@
value="custom" value="custom"
type="radio" type="radio"
name="default" name="default"
><i class="form-icon" /> {{ $t('message.customValue') }} ><i class="form-icon" /> {{ t('message.customValue') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -291,7 +291,7 @@
type="radio" type="radio"
name="default" name="default"
value="expression" value="expression"
><i class="form-icon" /> {{ $t('word.expression') }} ><i class="form-icon" /> {{ t('word.expression') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -306,7 +306,7 @@
<div v-if="customizations.onUpdate"> <div v-if="customizations.onUpdate">
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.onUpdate') }} {{ t('message.onUpdate') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -323,89 +323,78 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, nextTick, onMounted, Prop, PropType, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { TableField, TableIndex, TypesGroup } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTableRow',
components: { const props = defineProps({
ConfirmModal, row: Object as Prop<TableField>,
BaseSelect dataTypes: {
type: Array as PropType<TypesGroup[]>,
default: () => []
}, },
props: { indexes: Array as Prop<TableIndex[]>,
row: Object, foreigns: Array as Prop<string[]>,
dataTypes: { type: Array, default: () => [] },
indexes: Array,
foreigns: Array,
customizations: Object customizations: Object
}, });
emits: ['contextmenu', 'rename-field'],
setup () { const emit = defineEmits(['contextmenu', 'rename-field']);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore; const { getWorkspace } = workspacesStore;
return { const localRow: Ref<TableField> = ref({} as TableField);
addNotification, const isInlineEditor: Ref<TableField> = ref({} as TableField);
selectedWorkspace, const isDefaultModal = ref(false);
getWorkspace const defaultValue = ref({
};
},
data () {
return {
localRow: {},
isInlineEditor: {},
isDefaultModal: false,
defaultValue: {
type: 'noval', type: 'noval',
custom: '', custom: '',
expression: '', expression: '',
onUpdate: '' onUpdate: ''
}, });
editingContent: null, const editingContent: Ref<string | number> = ref(null);
originalContent: null, const originalContent = ref(null);
editingField: null const editingField: Ref<keyof TableField> = ref(null);
};
}, const localLength = computed(() => {
computed: { const localLength = localRow.value.numLength || localRow.value.charLength || localRow.value.datePrecision || localRow.value.numPrecision || 0 as number | true;
localLength () {
const localLength = this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
return localLength === true ? null : localLength; return localLength === true ? null : localLength;
}, });
fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type => const fieldType = computed(() => {
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : '') const fieldType = props.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
type.name === (localRow.value.type ? localRow.value.type.toUpperCase() : '')
); );
const group = this.dataTypes.filter(group => group.types.some(type => const group = props.dataTypes.filter(group => group.types.some(type =>
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : '')) type.name === (localRow.value.type ? localRow.value.type.toUpperCase() : ''))
); );
return fieldType.length ? { ...fieldType[0], group: group[0].group } : {}; return fieldType.length ? { ...fieldType[0], group: group[0].group } : {};
}, });
fieldDefault () {
if (this.localRow.autoIncrement) return 'AUTO_INCREMENT'; const fieldDefault = computed(() => {
if (this.localRow.default === 'NULL') return 'NULL'; if (localRow.value.autoIncrement) return 'AUTO_INCREMENT';
return this.localRow.default; if (localRow.value.default === 'NULL') return 'NULL';
}, return localRow.value.default;
collations () { });
return this.getWorkspace(this.selectedWorkspace).collations;
}, const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
canAutoincrement () { const canAutoincrement = computed(() => props.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type)));
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type)); const isNullable = computed(() => props.customizations.nullablePrimary || !props.indexes.some(index => ['PRIMARY'].includes(index.type)));
},
isNullable () { const isInDataTypes = computed(() => {
return this.customizations.nullablePrimary || !this.indexes.some(index => ['PRIMARY'].includes(index.type)); let typeNames: string[] = [];
}, for (const group of props.dataTypes) {
isInDataTypes () {
let typeNames = [];
for (const group of this.dataTypes) {
const groupTypeNames = group.types.reduce((acc, curr) => { const groupTypeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name); acc.push(curr.name);
return acc; return acc;
@ -413,195 +402,190 @@ export default {
typeNames = [...groupTypeNames, ...typeNames]; typeNames = [...groupTypeNames, ...typeNames];
} }
return typeNames.includes(this.row.type); return typeNames.includes(props.row.type);
}, });
types () {
const types = [...this.dataTypes]; const types = computed(() => {
if (!this.isInDataTypes) const types = [...props.dataTypes];
types.unshift({ name: this.row }); if (!isInDataTypes.value)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(types as any).unshift({ name: props.row });
return types; return types;
} });
},
watch: {
localRow () {
this.initLocalRow();
},
row () {
this.localRow = this.row;
},
indexes () {
if (!this.canAutoincrement)
this.localRow.autoIncrement = false;
if (!this.isNullable) const typeClass = (type: string) => {
this.localRow.nullable = false;
}
},
mounted () {
this.localRow = this.row;
this.initLocalRow();
this.isInlineEditor.length = false;
},
methods: {
keyName (key) {
switch (key) {
case 'pri':
return 'PRIMARY';
case 'uni':
return 'UNIQUE';
case 'mul':
return 'INDEX';
default:
return 'UNKNOWN ' + key;
}
},
typeClass (type) {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return ''; return '';
}, };
initLocalRow () {
Object.keys(this.localRow).forEach(key => { const initLocalRow = () => {
this.isInlineEditor[key] = false; Object.keys(localRow.value).forEach(key => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(isInlineEditor as any).value[key] = false;
}); });
this.defaultValue.onUpdate = this.localRow.onUpdate; defaultValue.value.onUpdate = localRow.value.onUpdate;
this.defaultValue.type = this.localRow.defaultType || 'noval'; defaultValue.value.type = localRow.value.defaultType || 'noval';
if (this.defaultValue.type === 'custom') { if (defaultValue.value.type === 'custom') {
this.defaultValue.custom = this.localRow.default defaultValue.value.custom = localRow.value.default
? this.localRow.default.includes('\'') ? localRow.value.default.includes('\'')
? this.localRow.default.split('\'')[1] ? localRow.value.default.split('\'')[1]
: this.localRow.default : localRow.value.default
: ''; : '';
} }
else if (this.defaultValue.type === 'expression') { else if (defaultValue.value.type === 'expression') {
if (this.localRow.default.toUpperCase().includes('ON UPDATE')) if (localRow.value.default.toUpperCase().includes('ON UPDATE'))
this.defaultValue.expression = this.localRow.default.replace(/ on update.*$/i, ''); defaultValue.value.expression = localRow.value.default.replace(/ on update.*$/i, '');
else else
this.defaultValue.expression = this.localRow.default; defaultValue.value.expression = localRow.value.default;
} }
}, };
editON (event, content, field) {
if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
else if (['string', 'unknown'].includes(this.fieldType.group)) this.editingField = 'charLength';
else if (['other'].includes(this.fieldType.group)) this.editingField = 'enumValues';
else if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
}
else
this.editingField = field;
if (this.localRow.enumValues && field === 'length') { const editON = async (event: MouseEvent, content: string | number, field: keyof TableField) => {
this.editingContent = this.localRow.enumValues; if (field === 'length') {
this.originalContent = this.localRow.enumValues; if (['integer', 'float', 'binary', 'spatial'].includes(fieldType.value.group)) editingField.value = 'numLength';
else if (['string', 'unknown'].includes(fieldType.value.group)) editingField.value = 'charLength';
else if (['other'].includes(fieldType.value.group)) editingField.value = 'enumValues';
else if (['time'].includes(fieldType.value.group)) editingField.value = 'datePrecision';
} }
else if (this.fieldType.scale && field === 'length') { else
const scale = this.localRow.numScale !== null ? this.localRow.numScale : 0; editingField.value = field;
this.editingContent = `${content}, ${scale}`;
this.originalContent = `${content}, ${scale}`; if (localRow.value.enumValues && field === 'length') {
editingContent.value = localRow.value.enumValues;
originalContent.value = localRow.value.enumValues;
}
else if (fieldType.value.scale && field === 'length') {
const scale = localRow.value.numScale !== null ? localRow.value.numScale : 0;
editingContent.value = `${content}, ${scale}`;
originalContent.value = `${content}, ${scale}`;
} }
else { else {
this.editingContent = content; editingContent.value = content;
this.originalContent = content; originalContent.value = content;
} }
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; isInlineEditor.value = { ...isInlineEditor.value, ...obj };
if (field === 'default') if (field === 'default')
this.isDefaultModal = true; isDefaultModal.value = true;
else { else {
this.$nextTick(() => { // Focus on input await nextTick();
event.target.blur(); (event as MouseEvent & { target: HTMLInputElement }).target.blur();
await nextTick();
this.$nextTick(() => document.querySelector('.editable-field').focus()); document.querySelector<HTMLInputElement>('.editable-field').focus();
});
} }
}, };
editOFF () {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
if (this.editingField === 'numLength' && this.fieldType.scale) { const editOFF = () => {
const [length, scale] = this.editingContent.split(','); if (editingField.value === 'name')
this.localRow.numLength = +length; emit('rename-field', { old: localRow.value[editingField.value], new: editingContent.value });
this.localRow.numScale = scale ? +scale : null;
if (editingField.value === 'numLength' && fieldType.value.scale) {
const [length, scale] = (editingContent.value as string).split(',');
localRow.value.numLength = +length;
localRow.value.numScale = scale ? +scale : null;
} }
else else
this.localRow[this.editingField] = this.editingContent; // eslint-disable-next-line @typescript-eslint/no-explicit-any
(localRow.value as any)[editingField.value] = editingContent.value;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) { if (editingField.value === 'type' && editingContent.value !== originalContent.value) {
this.localRow.numLength = null; localRow.value.numLength = null;
this.localRow.numScale = null; localRow.value.numScale = null;
this.localRow.charLength = null; localRow.value.charLength = null;
this.localRow.datePrecision = null; localRow.value.datePrecision = null;
this.localRow.enumValues = ''; localRow.value.enumValues = '';
if (this.fieldType.length) { if (fieldType.value.length) {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11; if (['integer', 'float', 'binary', 'spatial'].includes(fieldType.value.group)) localRow.value.numLength = 11;
if (['string'].includes(this.fieldType.group)) this.localRow.charLength = 15; if (['string'].includes(fieldType.value.group)) localRow.value.charLength = 15;
if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0; if (['time'].includes(fieldType.value.group)) localRow.value.datePrecision = 0;
if (['other'].includes(this.fieldType.group)) this.localRow.enumValues = '\'valA\',\'valB\''; if (['other'].includes(fieldType.value.group)) localRow.value.enumValues = '\'valA\',\'valB\'';
} }
if (!this.fieldType.collation) if (!fieldType.value.collation)
this.localRow.collation = null; localRow.value.collation = null;
if (!this.fieldType.unsigned) if (!fieldType.value.unsigned)
this.localRow.unsigned = false; localRow.value.unsigned = false;
if (!this.fieldType.zerofill) if (!fieldType.value.zerofill)
this.localRow.zerofill = false; localRow.value.zerofill = false;
} }
else if (this.editingField === 'default') { else if (editingField.value === 'default') {
switch (this.defaultValue.type) { switch (defaultValue.value.type) {
case 'autoincrement': case 'autoincrement':
this.localRow.autoIncrement = true; localRow.value.autoIncrement = true;
break; break;
case 'noval': case 'noval':
this.localRow.autoIncrement = false; localRow.value.autoIncrement = false;
this.localRow.default = null; localRow.value.default = null;
break; break;
case 'null': case 'null':
this.localRow.autoIncrement = false; localRow.value.autoIncrement = false;
this.localRow.default = 'NULL'; localRow.value.default = 'NULL';
break; break;
case 'custom': case 'custom':
this.localRow.autoIncrement = false; localRow.value.autoIncrement = false;
this.localRow.default = Number.isNaN(+this.defaultValue.custom) ? `'${this.defaultValue.custom}'` : this.defaultValue.custom; localRow.value.default = Number.isNaN(+defaultValue.value.custom) ? `'${defaultValue.value.custom}'` : defaultValue.value.custom;
break; break;
case 'expression': case 'expression':
this.localRow.autoIncrement = false; localRow.value.autoIncrement = false;
this.localRow.default = this.defaultValue.expression; localRow.value.default = defaultValue.value.expression;
break; break;
} }
this.localRow.onUpdate = this.defaultValue.onUpdate; localRow.value.onUpdate = defaultValue.value.onUpdate;
} }
Object.keys(this.isInlineEditor).forEach(key => { Object.keys(isInlineEditor.value).forEach(key => {
this.isInlineEditor = { ...this.isInlineEditor, [key]: false }; isInlineEditor.value = { ...isInlineEditor.value, [key]: false };
}); });
this.editingContent = null; editingContent.value = null;
this.originalContent = null; originalContent.value = null;
this.editingField = null; editingField.value = null;
}, };
checkLengthScale (e) {
e = (e) || window.event; const checkLengthScale = (e: KeyboardEvent & { target: HTMLInputElement }) => {
const charCode = (e.which) ? e.which : e.keyCode; e = (e) || window.event as KeyboardEvent & { target: HTMLInputElement };
const charCode = (e.which) ? e.which : e.code;
if (((charCode > 31 && (charCode < 48 || charCode > 57)) && charCode !== 44) || (charCode === 44 && e.target.value.includes(','))) if (((charCode > 31 && (charCode < 48 || charCode > 57)) && charCode !== 44) || (charCode === 44 && e.target.value.includes(',')))
e.preventDefault(); e.preventDefault();
else else
return true; return true;
},
hideDefaultModal () {
this.isDefaultModal = false;
}
}
}; };
const hideDefaultModal = () => {
isDefaultModal.value = false;
};
watch(localRow, () => {
initLocalRow();
});
watch(() => props.row, () => {
localRow.value = props.row;
});
watch(() => props.indexes, () => {
if (!canAutoincrement.value)
localRow.value.autoIncrement = false;
if (!isNullable.value)
localRow.value.nullable = false;
});
onMounted(() => {
localRow.value = props.row;
initLocalRow();
isInlineEditor.value.length = false;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,20 +11,20 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -34,7 +34,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
v-model="localTrigger.name" v-model="localTrigger.name"
class="form-input" class="form-input"
@ -44,12 +44,12 @@
</div> </div>
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localTrigger.definer" v-model="localTrigger.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -57,7 +57,7 @@
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename"> <fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label> <label class="form-label">{{ t('word.table') }}</label>
<BaseSelect <BaseSelect
v-model="localTrigger.table" v-model="localTrigger.table"
:options="schemaTables" :options="schemaTables"
@ -69,7 +69,7 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label> <label class="form-label">{{ t('word.event') }}</label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
v-model="localTrigger.activation" v-model="localTrigger.activation"
@ -85,7 +85,7 @@
<div v-if="customizations.triggerMultipleEvents" class="px-4"> <div v-if="customizations.triggerMultipleEvents" class="px-4">
<label <label
v-for="event in Object.keys(localEvents)" v-for="event in (Object.keys(localEvents) as ('INSERT' | 'UPDATE' | 'DELETE')[])"
:key="event" :key="event"
class="form-checkbox form-inline" class="form-checkbox form-inline"
@change.prevent="changeEvents(event)" @change.prevent="changeEvents(event)"
@ -101,7 +101,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label> <label class="form-label ml-2">{{ t('message.triggerStatement') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -114,202 +114,143 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { type TriggerEventName = 'INSERT' | 'UPDATE' | 'DELETE'
name: 'WorkspaceTabPropsTrigger',
components: { const { t } = useI18n();
BaseLoader,
QueryEditor, const props = defineProps({
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
trigger: String, trigger: String,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
renameTabs, renameTabs,
newTab,
changeBreadcrumbs, changeBreadcrumbs,
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const lastTrigger = ref(null);
selectedWorkspace, const isLoading = ref(false);
getWorkspace, const isSaving = ref(false);
refreshStructure, const originalTrigger = ref(null);
renameTabs, const localTrigger = ref(null);
newTab, const editorHeight = ref(300);
changeBreadcrumbs, const localEvents = ref({ INSERT: false, UPDATE: false, DELETE: false });
setUnsavedChanges
}; const workspace = computed(() => {
}, return getWorkspace(props.connection.uid);
data () { });
return {
isLoading: false, const customizations = computed(() => {
isSaving: false, return workspace.value.customizations;
originalTrigger: null, });
localTrigger: { sql: '' },
lastTrigger: null, const isChanged = computed(() => {
sqlProxy: '', return JSON.stringify(originalTrigger.value) !== JSON.stringify(localTrigger.value);
editorHeight: 300, });
localEvents: { INSERT: false, UPDATE: false, DELETE: false }
}; const isDefinerInUsers = computed(() => {
}, return originalTrigger.value ? workspace.value.users.some(user => originalTrigger.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
computed: { });
workspace () {
return this.getWorkspace(this.connection.uid); const schemaTables = computed(() => {
}, const schemaTables = workspace.value.structure
customizations () { .filter(schema => schema.name === props.schema)
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables); .map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}, });
users () {
const users = [{ value: '' }, ...this.workspace.users]; const users = computed(() => {
if (!this.isDefinerInUsers) { const users = [{ value: '' }, ...workspace.value.users];
const [name, host] = this.originalTrigger.definer.replaceAll('`', '').split('@'); if (!isDefinerInUsers.value) {
const [name, host] = originalTrigger.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host }); users.unshift({ name, host });
} }
return users; return users;
}
},
watch: {
async schema () {
if (this.isSelected) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
async trigger () {
if (this.isSelected) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, trigger: this.trigger });
setTimeout(() => {
this.resizeQueryEditor();
}, 200);
if (this.lastTrigger !== this.trigger)
this.getTriggerData();
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getTriggerData () {
if (!this.trigger) return;
Object.keys(this.localEvents).forEach(event => {
this.localEvents[event] = false;
}); });
this.localTrigger = { sql: '' }; const getTriggerData = async () => {
this.isLoading = true; if (!props.trigger) return;
this.lastTrigger = this.trigger;
Object.keys(localEvents.value).forEach((event: TriggerEventName) => {
localEvents.value[event] = false;
});
localTrigger.value = { sql: '' };
isLoading.value = true;
lastTrigger.value = props.trigger;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
trigger: this.trigger trigger: props.trigger
}; };
try { try {
const { status, response } = await Triggers.getTriggerInformations(params); const { status, response } = await Triggers.getTriggerInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalTrigger = response; originalTrigger.value = response;
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger)); localTrigger.value = JSON.parse(JSON.stringify(originalTrigger.value));
this.sqlProxy = this.localTrigger.sql;
if (this.customizations.triggerMultipleEvents) { if (customizations.value.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => { originalTrigger.value.event.forEach((event: TriggerEventName) => {
this.localEvents[e] = true; localEvents.value[event] = true;
}); });
} }
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) { const changeEvents = (event: TriggerEventName) => {
this.localEvents[event] = !this.localEvents[event]; if (customizations.value.triggerMultipleEvents) {
this.localTrigger.event = []; localEvents.value[event] = !localEvents.value[event];
for (const key in this.localEvents) { localTrigger.value.event = [];
if (this.localEvents[key]) for (const key in localEvents.value) {
this.localTrigger.event.push(key); if (localEvents.value[key as TriggerEventName])
localTrigger.value.event.push(key);
} }
} }
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
trigger: { trigger: {
...this.localTrigger, ...localTrigger.value,
schema: this.schema, schema: props.schema,
oldName: this.originalTrigger.name oldName: originalTrigger.value.name
} }
}; };
@ -317,65 +258,108 @@ export default {
const { status, response } = await Triggers.alterTrigger(params); const { status, response } = await Triggers.alterTrigger(params);
if (status === 'success') { if (status === 'success') {
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (this.originalTrigger.name !== this.localTrigger.name) { if (originalTrigger.value.name !== localTrigger.value.name) {
const triggerName = this.customizations.triggerTableInName ? `${this.localTrigger.table}.${this.localTrigger.name}` : this.localTrigger.name; const triggerName = customizations.value.triggerTableInName ? `${localTrigger.value.table}.${localTrigger.value.name}` : localTrigger.value.name;
const triggerOldName = this.customizations.triggerTableInName ? `${this.originalTrigger.table}.${this.originalTrigger.name}` : this.originalTrigger.name; const triggerOldName = customizations.value.triggerTableInName ? `${originalTrigger.value.table}.${originalTrigger.value.name}` : originalTrigger.value.name;
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: triggerOldName, elementName: triggerOldName,
elementNewName: triggerName, elementNewName: triggerName,
elementType: 'trigger' elementType: 'trigger'
}); });
this.changeBreadcrumbs({ schema: this.schema, trigger: triggerName }); changeBreadcrumbs({ schema: props.schema, trigger: triggerName });
} }
else else
this.getTriggerData(); getTriggerData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
Object.keys(this.localEvents).forEach(event => { const clearChanges = () => {
this.localEvents[event] = false; localTrigger.value = JSON.parse(JSON.stringify(originalTrigger.value));
queryEditor.value.editor.session.setValue(localTrigger.value.sql);
Object.keys(localEvents.value).forEach((event: TriggerEventName) => {
localEvents.value[event] = false;
}); });
if (this.customizations.triggerMultipleEvents) { if (customizations.value.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => { originalTrigger.value.event.forEach((event: TriggerEventName) => {
this.localEvents[e] = true; localEvents.value[event] = true;
}); });
} }
}, };
resizeQueryEditor () {
if (this.$refs.queryEditor) { const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
onKey (e) {
if (this.isSelected) { const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getTriggerData();
queryEditor.value.editor.session.setValue(localTrigger.value.sql);
lastTrigger.value = props.trigger;
}
});
watch(() => props.trigger, async () => {
if (props.isSelected) {
await getTriggerData();
queryEditor.value.editor.session.setValue(localTrigger.value.sql);
lastTrigger.value = props.trigger;
}
});
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getTriggerData();
queryEditor.value.editor.session.setValue(localTrigger.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -30,7 +30,7 @@
<div v-if="customizations.triggerFunctionlanguages" class="column col-auto"> <div v-if="customizations.triggerFunctionlanguages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -42,20 +42,20 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="workspace.users" :options="workspace.users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div v-if="customizations.comment" class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -67,7 +67,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -77,295 +77,194 @@
:height="editorHeight" :height="editorHeight"
/> />
</div> </div>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
:client="workspace.client"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import BaseLoader from '@/components/BaseLoader.vue';
import BaseLoader from '@/components/BaseLoader'; import QueryEditor from '@/components/QueryEditor.vue';
import QueryEditor from '@/components/QueryEditor';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { AlterFunctionParams, TriggerFunctionInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsTriggerFunction',
components: { const props = defineProps({
BaseLoader,
QueryEditor,
ModalAskParameters,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
function: String, function: String,
isSelected: Boolean, isSelected: Boolean,
schema: String schema: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
renameTabs, renameTabs,
newTab,
changeBreadcrumbs, changeBreadcrumbs,
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const isLoading = ref(false);
selectedWorkspace, const isSaving = ref(false);
getWorkspace, const originalFunction: Ref<TriggerFunctionInfos> = ref(null);
refreshStructure, const localFunction: Ref<TriggerFunctionInfos> = ref(null);
renameTabs, const lastFunction = ref(null);
newTab, const sqlProxy = ref('');
changeBreadcrumbs, const editorHeight = ref(300);
setUnsavedChanges
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const workspace = computed(() => getWorkspace(props.connection.uid));
} const customizations = computed(() => workspace.value.customizations);
}, const isChanged = computed(() => JSON.stringify(originalFunction.value) !== JSON.stringify(localFunction.value));
watch: {
async schema () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async function () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, triggerFunction: this.function });
setTimeout(() => { const getFunctionData = async () => {
this.resizeQueryEditor(); if (!props.function) return;
}, 200);
if (this.lastFunction !== this.function) isLoading.value = true;
await this.getFunctionData(); localFunction.value = { name: '', sql: '', type: '', definer: null };
} lastFunction.value = props.function;
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getFunctionData () {
if (!this.function) return;
this.isLoading = true;
this.localFunction = { sql: '' };
this.lastFunction = this.function;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
func: this.function func: props.function
}; };
try { try {
const { status, response } = await Functions.getFunctionInformations(params); const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalFunction = response; originalFunction.value = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => { localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
param._antares_id = uidGen(); sqlProxy.value = localFunction.value.sql;
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
func: { func: {
...this.localFunction, ...localFunction.value,
schema: this.schema, schema: props.schema,
oldName: this.originalFunction.name oldName: originalFunction.value.name
} } as AlterFunctionParams
}; };
try { try {
const { status, response } = await Functions.alterTriggerFunction(params); const { status, response } = await Functions.alterTriggerFunction(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.originalFunction.name; const oldName = originalFunction.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localFunction.name) { if (oldName !== localFunction.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localFunction.name, elementNewName: localFunction.value.name,
elementType: 'triggerFunction' elementType: 'triggerFunction'
}); });
this.changeBreadcrumbs({ schema: this.schema, triggerFunction: this.localFunction.name }); changeBreadcrumbs({ schema: props.schema, triggerFunction: localFunction.value.name });
} }
else else
this.getFunctionData(); getFunctionData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); const clearChanges = () => {
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql); localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
}, queryEditor.value.editor.session.setValue(localFunction.value.sql);
resizeQueryEditor () { };
if (this.$refs.queryEditor) {
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql; const onKey = (e: KeyboardEvent) => {
switch (this.connection.client) { // TODO: move in a better place if (props.isSelected) {
case 'maria':
case 'mysql':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.function, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -11,20 +11,20 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -34,7 +34,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
v-model="localView.name" v-model="localView.name"
class="form-input" class="form-input"
@ -44,19 +44,19 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group"> <div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localView.definer" v-model="localView.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group"> <div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label> <label class="form-label">{{ t('message.sqlSecurity') }}</label>
<BaseSelect <BaseSelect
v-model="localView.security" v-model="localView.security"
:options="['DEFINER', 'INVOKER']" :options="['DEFINER', 'INVOKER']"
@ -66,7 +66,7 @@
</div> </div>
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group"> <div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label> <label class="form-label">{{ t('word.algorithm') }}</label>
<BaseSelect <BaseSelect
v-model="localView.algorithm" v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']" :options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
@ -76,10 +76,10 @@
</div> </div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2"> <div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label> <label class="form-label">{{ t('message.updateOption') }}</label>
<BaseSelect <BaseSelect
v-model="localView.updateOption" v-model="localView.updateOption"
:option-track-by="(user) => user.value" :option-track-by="(user: any) => user.value"
:options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]" :options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]"
class="form-select" class="form-select"
/> />
@ -89,7 +89,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label> <label class="form-label ml-2">{{ t('message.selectStatement') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -102,35 +102,30 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import Views from '@/ipc-api/Views';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsView',
components: { const props = defineProps({
BaseLoader,
QueryEditor,
BaseSelect
},
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object,
isSelected: Boolean, isSelected: Boolean,
schema: String, schema: String,
view: String view: String
}, });
setup () {
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
@ -139,131 +134,68 @@ export default {
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const isLoading = ref(false);
selectedWorkspace, const isSaving = ref(false);
getWorkspace, const originalView = ref(null);
refreshStructure, const localView = ref(null);
renameTabs, const editorHeight = ref(300);
changeBreadcrumbs, const lastView = ref(null);
setUnsavedChanges const sqlProxy = ref('');
};
}, const workspace = computed(() => getWorkspace(props.connection.uid));
data () { const isChanged = computed(() => JSON.stringify(originalView.value) !== JSON.stringify(localView.value));
return { const isDefinerInUsers = computed(() => originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true);
isLoading: false,
isSaving: false, const users = computed(() => {
originalView: null, const users = [{ value: '' }, ...workspace.value.users];
localView: { sql: '' }, if (!isDefinerInUsers.value) {
lastView: null, const [name, host] = originalView.value.definer.replaceAll('`', '').split('@');
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
isChanged () {
return JSON.stringify(this.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
users () {
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) {
const [name, host] = this.originalView.definer.replaceAll('`', '').split('@');
users.unshift({ name, host }); users.unshift({ name, host });
} }
return users; return users;
} });
},
watch: {
async schema () {
if (this.isSelected) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
async view () {
if (this.isSelected) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, view: this.view });
setTimeout(() => { const getViewData = async () => {
this.resizeQueryEditor(); if (!props.view) return;
}, 200); isLoading.value = true;
localView.value = { sql: '' };
if (this.lastView !== this.view) lastView.value = props.view;
this.getViewData();
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getViewData () {
if (!this.view) return;
this.isLoading = true;
this.localView = { sql: '' };
this.lastView = this.view;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
view: this.view view: props.view
}; };
try { try {
const { status, response } = await Views.getViewInformations(params); const { status, response } = await Views.getViewInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalView = response; originalView.value = response;
this.localView = JSON.parse(JSON.stringify(this.originalView)); localView.value = JSON.parse(JSON.stringify(originalView.value));
this.sqlProxy = this.localView.sql; sqlProxy.value = localView.value.sql;
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.resizeQueryEditor(); resizeQueryEditor();
this.isLoading = false; isLoading.value = false;
}, };
async saveChanges () {
if (this.isSaving) return; const saveChanges = async () => {
this.isSaving = true; if (isSaving.value) return;
isSaving.value = true;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
view: { view: {
...this.localView, ...localView.value,
schema: this.schema, schema: props.schema,
oldName: this.originalView.name oldName: originalView.value.name
} }
}; };
@ -271,54 +203,103 @@ export default {
const { status, response } = await Views.alterView(params); const { status, response } = await Views.alterView(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.originalView.name; const oldName = originalView.value.name;
await this.refreshStructure(this.connection.uid); await refreshStructure(props.connection.uid);
if (oldName !== this.localView.name) { if (oldName !== localView.value.name) {
this.renameTabs({ renameTabs({
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
elementName: oldName, elementName: oldName,
elementNewName: this.localView.name, elementNewName: localView.value.name,
elementType: 'view' elementType: 'view'
}); });
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name }); changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
} }
else else
this.getViewData(); getViewData();
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isSaving = false; isSaving.value = false;
}, };
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView)); const clearChanges = () => {
this.$refs.queryEditor.editor.session.setValue(this.localView.sql); localView.value = JSON.parse(JSON.stringify(originalView.value));
}, queryEditor.value.editor.session.setValue(localView.value.sql);
resizeQueryEditor () { };
if (this.$refs.queryEditor) {
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size; editorHeight.value = size;
this.$refs.queryEditor.editor.resize(); queryEditor.value.editor.resize();
} }
}, };
onKey (e) {
if (this.isSelected) { const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S if (e.ctrlKey && e.key === 's') { // CTRL + S
if (this.isChanged) if (isChanged.value)
this.saveChanges(); saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
lastView.value = props.view;
}
});
watch(() => props.view, async () => {
if (props.isSelected) {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
lastView.value = props.view;
}
});
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
setTimeout(() => {
resizeQueryEditor();
}, 50);
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -28,11 +28,11 @@
v-if="showCancel && isQuering" v-if="showCancel && isQuering"
class="btn btn-primary btn-sm cancellable" class="btn btn-primary btn-sm cancellable"
:disabled="!query" :disabled="!query"
:title="$t('word.cancel')" :title="t('word.cancel')"
@click="killTabQuery()" @click="killTabQuery()"
> >
<i class="mdi mdi-24px mdi-window-close" /> <i class="mdi mdi-24px mdi-window-close" />
<span class="d-invisible pr-1">{{ $t('word.run') }}</span> <span class="d-invisible pr-1">{{ t('word.run') }}</span>
</button> </button>
<button <button
v-else v-else
@ -43,7 +43,7 @@
@click="runQuery(query)" @click="runQuery(query)"
> >
<i class="mdi mdi-24px mdi-play pr-1" /> <i class="mdi mdi-24px mdi-play pr-1" />
<span>{{ $t('word.run') }}</span> <span>{{ t('word.run') }}</span>
</button> </button>
</div> </div>
<button <button
@ -53,7 +53,7 @@
@click="commitTab()" @click="commitTab()"
> >
<i class="mdi mdi-24px mdi-cube-send pr-1" /> <i class="mdi mdi-24px mdi-cube-send pr-1" />
<span>{{ $t('word.commit') }}</span> <span>{{ t('word.commit') }}</span>
</button> </button>
<button <button
v-if="!autocommit" v-if="!autocommit"
@ -62,7 +62,7 @@
@click="rollbackTab()" @click="rollbackTab()"
> >
<i class="mdi mdi-24px mdi-undo-variant pr-1" /> <i class="mdi mdi-24px mdi-undo-variant pr-1" />
<span>{{ $t('word.rollback') }}</span> <span>{{ t('word.rollback') }}</span>
</button> </button>
<button <button
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
@ -71,7 +71,7 @@
@click="clear()" @click="clear()"
> >
<i class="mdi mdi-24px mdi-delete-sweep pr-1" /> <i class="mdi mdi-24px mdi-delete-sweep pr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -83,7 +83,7 @@
@click="beautify()" @click="beautify()"
> >
<i class="mdi mdi-24px mdi-brush pr-1" /> <i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span> <span>{{ t('word.format') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
@ -92,7 +92,7 @@
@click="openHistoryModal()" @click="openHistoryModal()"
> >
<i class="mdi mdi-24px mdi-history pr-1" /> <i class="mdi mdi-24px mdi-history pr-1" />
<span>{{ $t('word.history') }}</span> <span>{{ t('word.history') }}</span>
</button> </button>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
<button <button
@ -101,7 +101,7 @@
tabindex="0" tabindex="0"
> >
<i class="mdi mdi-24px mdi-file-export mr-1" /> <i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span> <span>{{ t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" /> <i class="mdi mdi-24px mdi-menu-down" />
</button> </button>
<ul class="menu text-left"> <ul class="menu text-left">
@ -113,13 +113,13 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="input-group pr-2" :title="$t('message.commitMode')"> <div class="input-group pr-2" :title="t('message.commitMode')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-source-commit p-0" /> <i class="input-group-addon addon-sm mdi mdi-24px mdi-source-commit p-0" />
<BaseSelect <BaseSelect
v-model="autocommit" v-model="autocommit"
:options="[{value: true, label: $t('message.autoCommit')}, {value: false, label: $t('message.manualCommit')}]" :options="[{value: true, label: t('message.autoCommit')}, {value: false, label: t('message.manualCommit')}]"
:option-label="opt => opt.label" :option-label="(opt: any) => opt.label"
:option-track-by="opt => opt.value" :option-track-by="(opt: any) => opt.value"
class="form-select select-sm text-bold" class="form-select select-sm text-bold"
/> />
</div> </div>
@ -128,30 +128,30 @@
<div <div
v-if="results.length" v-if="results.length"
class="d-flex" class="d-flex"
:title="$t('message.queryDuration')" :title="t('message.queryDuration')"
> >
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b> <i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div> </div>
<div <div
v-if="resultsCount" v-if="resultsCount"
class="d-flex" class="d-flex"
:title="$t('word.results')" :title="t('word.results')"
> >
<i class="mdi mdi-equal pr-1" /> <b>{{ resultsCount.toLocaleString() }}</b> <i class="mdi mdi-equal pr-1" /> <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
<div <div
v-if="hasAffected" v-if="hasAffected"
class="d-flex" class="d-flex"
:title="$t('message.affectedRows')" :title="t('message.affectedRows')"
> >
<i class="mdi mdi-target pr-1" /> <b>{{ affectedCount }}</b> <i class="mdi mdi-target pr-1" /> <b>{{ affectedCount }}</b>
</div> </div>
<div class="input-group" :title="$t('word.schema')"> <div class="input-group" :title="t('word.schema')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-database" /> <i class="input-group-addon addon-sm mdi mdi-24px mdi-database" />
<BaseSelect <BaseSelect
v-model="selectedSchema" v-model="selectedSchema"
:options="[{value: null, label: $t('message.noSchema')}, ...databaseSchemas.map(el => ({label: el, value: el}))]" :options="[{value: null, label: t('message.noSchema')}, ...databaseSchemas.map(el => ({label: el, value: el}))]"
class="form-select select-sm text-bold" class="form-select select-sm text-bold"
/> />
</div> </div>
@ -183,45 +183,46 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, Prop, ref, Ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { format } from 'sql-formatter'; import { format } from 'sql-formatter';
import { ConnectionParams } from 'common/interfaces/antares';
import { useHistoryStore } from '@/stores/history'; import { useHistoryStore } from '@/stores/history';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState'; import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState.vue';
import ModalHistory from '@/components/ModalHistory'; import ModalHistory from '@/components/ModalHistory.vue';
import tableTabs from '@/mixins/tableTabs'; import { useResultTables } from '@/composables/useResultTables';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabQuery',
components: { const props = defineProps({
BaseLoader,
QueryEditor,
WorkspaceTabQueryTable,
WorkspaceTabQueryEmptyState,
ModalHistory,
BaseSelect
},
mixins: [tableTabs],
props: {
tabUid: String, tabUid: String,
connection: Object, connection: Object as Prop<ConnectionParams>,
tab: Object, tab: Object,
isSelected: Boolean isSelected: Boolean
}, });
setup () {
const { getHistoryByWorkspace, saveHistory } = useHistoryStore(); const reloadTable = () => runQuery(lastQuery.value);
const {
queryTable,
isQuering,
updateField,
deleteSelected
} = useResultTables(props.connection.uid, reloadTable);
const { saveHistory } = useHistoryStore();
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { const {
getWorkspace, getWorkspace,
changeBreadcrumbs, changeBreadcrumbs,
@ -229,228 +230,174 @@ export default {
setUnsavedChanges setUnsavedChanges
} = workspacesStore; } = workspacesStore;
return { const queryEditor: Ref<Component & { editor: Ace.Editor; $el: HTMLElement }> = ref(null);
getHistoryByWorkspace, const queryAreaFooter: Ref<HTMLDivElement> = ref(null);
saveHistory, const resizer: Ref<HTMLDivElement> = ref(null);
addNotification, const query = ref('');
selectedWorkspace, const lastQuery = ref('');
getWorkspace, const isCancelling = ref(false);
changeBreadcrumbs, const showCancel = ref(false);
updateTabContent, const autocommit = ref(true);
setUnsavedChanges const results = ref([]);
}; const selectedSchema = ref(null);
}, const resultsCount = ref(0);
data () { const durationsCount = ref(0);
return { const affectedCount = ref(null);
query: '', const editorHeight = ref(200);
lastQuery: '', const isHistoryOpen = ref(false);
isQuering: false, const debounceTimeout = ref(null);
isCancelling: false,
showCancel: false, const workspace = computed(() => getWorkspace(props.connection.uid));
autocommit: true, const breadcrumbsSchema = computed(() => workspace.value.breadcrumbs.schema || null);
results: [], const databaseSchemas = computed(() => {
selectedSchema: null, return workspace.value.structure.reduce((acc, curr) => {
resultsCount: 0,
durationsCount: 0,
affectedCount: null,
editorHeight: 200,
isHistoryOpen: false,
debounceTimeout: null
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
breadcrumbsSchema () {
return this.workspace.breadcrumbs.schema || null;
},
databaseSchemas () {
return this.workspace.structure.reduce((acc, curr) => {
acc.push(curr.name); acc.push(curr.name);
return acc; return acc;
}, []); }, []);
}, });
isWorkspaceSelected () { const hasResults = computed(() => results.value.length && results.value[0].rows);
return this.workspace.uid === this.selectedWorkspace; const hasAffected = computed(() => affectedCount.value || (!resultsCount.value && affectedCount.value !== null));
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
hasResults () {
return this.results.length && this.results[0].rows;
},
hasAffected () {
return this.affectedCount || (!this.resultsCount && this.affectedCount !== null);
}
},
watch: {
query (val) {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => { watch(query, (val) => {
this.updateTabContent({ clearTimeout(debounceTimeout.value);
uid: this.connection.uid,
tab: this.tab.uid, debounceTimeout.value = setTimeout(() => {
updateTabContent({
uid: props.connection.uid,
tab: props.tab.uid,
type: 'query', type: 'query',
schema: this.selectedSchema, schema: selectedSchema.value,
content: val content: val
}); });
}, 200); }, 200);
},
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
setTimeout(() => {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.focus();
}, 0);
}
},
selectedSchema () {
this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
}
},
created () {
this.query = this.tab.content;
this.selectedSchema = this.tab.schema || this.breadcrumbsSchema;
if (!this.databaseSchemas.includes(this.selectedSchema))
this.selectedSchema = null;
window.addEventListener('keydown', this.onKey);
window.addEventListener('resize', this.onWindowResize);
},
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', e => {
e.preventDefault();
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
}); });
if (this.tab.autorun) watch(() => props.isSelected, (val) => {
this.runQuery(this.query); if (val) {
}, changeBreadcrumbs({ schema: selectedSchema.value, query: `Query #${props.tab.index}` });
beforeUnmount () { setTimeout(() => {
window.removeEventListener('resize', this.onWindowResize); if (queryEditor.value)
window.removeEventListener('keydown', this.onKey); queryEditor.value.editor.focus();
const params = { }, 0);
uid: this.connection.uid, }
tabUid: this.tab.uid });
};
Schema.destroyConnectionToCommit(params); watch(selectedSchema, () => {
}, changeBreadcrumbs({ schema: selectedSchema.value, query: `Query #${props.tab.index}` });
methods: { });
async runQuery (query) {
if (!query || this.isQuering) return; const runQuery = async (query: string) => {
this.isQuering = true; if (!query || isQuering.value) return;
this.clearTabData(); isQuering.value = true;
this.$refs.queryTable.resetSort(); clearTabData();
queryTable.value.resetSort();
try { // Query Data try { // Query Data
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.selectedSchema, schema: selectedSchema.value,
tabUid: this.tab.uid, tabUid: props.tab.uid,
autocommit: this.autocommit, autocommit: autocommit.value,
query query
}; };
const { status, response } = await Schema.rawQuery(params); const { status, response } = await Schema.rawQuery(params);
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; results.value = Array.isArray(response) ? response : [response];
this.resultsCount = this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0); resultsCount.value = results.value.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount = this.results.reduce((acc, curr) => acc + curr.duration, 0); durationsCount.value = results.value.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount = this.results affectedCount.value = results.value
.filter(result => result.report !== null) .filter(result => result.report !== null)
.reduce((acc, curr) => { .reduce((acc, curr) => {
if (acc === null) acc = 0; if (acc === null) acc = 0;
return acc + (curr.report ? curr.report.affectedRows : 0); return acc + (curr.report ? curr.report.affectedRows : 0);
}, null); }, null);
this.saveHistory(params); saveHistory(params);
if (!this.autocommit) if (!autocommit.value)
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: true }); setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: true });
} }
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isQuering = false; isQuering.value = false;
this.lastQuery = query; lastQuery.value = query;
}, };
async killTabQuery () {
if (this.isCancelling) return;
this.isCancelling = true; const killTabQuery = async () => {
if (isCancelling.value) return;
isCancelling.value = true;
try { try {
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
tabUid: this.tab.uid tabUid: props.tab.uid
}; };
await Schema.killTabQuery(params); await Schema.killTabQuery(params);
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isCancelling = false; isCancelling.value = false;
}, };
setCancelButtonVisibility (val) {
if (this.workspace.customizations.cancelQueries) const setCancelButtonVisibility = (val: boolean) => {
this.showCancel = val; if (workspace.value.customizations.cancelQueries)
}, showCancel.value = val;
reloadTable () { };
this.runQuery(this.lastQuery);
}, const clearTabData = () => {
clearTabData () { results.value = [];
this.results = []; resultsCount.value = 0;
this.resultsCount = 0; durationsCount.value = 0;
this.durationsCount = 0; affectedCount.value = null;
this.affectedCount = null; };
},
resize (e) { const resize = (e: MouseEvent) => {
const el = this.$refs.queryEditor.$el; const el = queryEditor.value.$el;
const queryFooterHeight = this.$refs.queryAreaFooter.clientHeight; const queryFooterHeight = queryAreaFooter.value.clientHeight;
const bottom = e.pageY || this.$refs.resizer.getBoundingClientRect().bottom; const bottom = e.pageY || resizer.value.getBoundingClientRect().bottom;
const maxHeight = window.innerHeight - 100 - queryFooterHeight; const maxHeight = window.innerHeight - 100 - queryFooterHeight;
let editorHeight = bottom - el.getBoundingClientRect().top; let localEditorHeight = bottom - el.getBoundingClientRect().top;
if (editorHeight > maxHeight) editorHeight = maxHeight; if (localEditorHeight > maxHeight) localEditorHeight = maxHeight;
if (editorHeight < 50) editorHeight = 50; if (localEditorHeight < 50) localEditorHeight = 50;
this.editorHeight = editorHeight; editorHeight.value = localEditorHeight;
}, };
onWindowResize (e) {
const el = this.$refs.queryEditor.$el; const onWindowResize = (e: MouseEvent) => {
const queryFooterHeight = this.$refs.queryAreaFooter.clientHeight; const el = queryEditor.value.$el;
const bottom = e.pageY || this.$refs.resizer.getBoundingClientRect().bottom; const queryFooterHeight = queryAreaFooter.value.clientHeight;
const bottom = e.pageY || resizer.value.getBoundingClientRect().bottom;
const maxHeight = window.innerHeight - 100 - queryFooterHeight; const maxHeight = window.innerHeight - 100 - queryFooterHeight;
const editorHeight = bottom - el.getBoundingClientRect().top; const localEditorHeight = bottom - el.getBoundingClientRect().top;
if (editorHeight > maxHeight) if (localEditorHeight > maxHeight)
this.editorHeight = maxHeight; editorHeight.value = maxHeight;
}, };
stopResize () {
window.removeEventListener('mousemove', this.resize);
if (this.$refs.queryTable && this.results.length)
this.$refs.queryTable.resizeResults();
if (this.$refs.queryEditor) const stopResize = () => {
this.$refs.queryEditor.editor.resize(); window.removeEventListener('mousemove', resize);
}, if (queryTable.value && results.value.length)
beautify () { queryTable.value.resizeResults();
if (this.$refs.queryEditor) {
let language = 'sql';
switch (this.workspace.client) { if (queryEditor.value)
queryEditor.value.editor.resize();
};
const beautify = () => {
if (queryEditor.value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let language: any = 'sql';
switch (workspace.value.client) {
case 'mysql': case 'mysql':
language = 'mysql'; language = 'mysql';
break; break;
@ -462,68 +409,105 @@ export default {
break; break;
} }
const formattedQuery = format(this.query, { const formattedQuery = format(query.value, {
language, language,
uppercase: true uppercase: true
}); });
this.$refs.queryEditor.editor.session.setValue(formattedQuery); queryEditor.value.editor.session.setValue(formattedQuery);
} }
}, };
openHistoryModal () {
this.isHistoryOpen = true;
},
selectQuery (sql) {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(sql);
this.isHistoryOpen = false; const openHistoryModal = () => {
}, isHistoryOpen.value = true;
clear () { };
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(''); const selectQuery = (sql: string) => {
this.clearTabData(); if (queryEditor.value)
}, queryEditor.value.editor.session.setValue(sql);
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`); isHistoryOpen.value = false;
}, };
async commitTab () {
this.isQuering = true; const clear = () => {
if (queryEditor.value)
queryEditor.value.editor.session.setValue('');
clearTabData();
};
const downloadTable = (format: 'csv' | 'json') => {
queryTable.value.downloadTable(format, `${props.tab.type}-${props.tab.index}`);
};
const commitTab = async () => {
isQuering.value = true;
try { try {
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
tabUid: this.tab.uid tabUid: props.tab.uid
}; };
await Schema.commitTab(params); await Schema.commitTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false }); setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'COMMIT' }) }); addNotification({ status: 'success', message: t('message.actionSuccessful', { action: 'COMMIT' }) });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isQuering = false; isQuering.value = false;
}, };
async rollbackTab () {
this.isQuering = true; const rollbackTab = async () => {
isQuering.value = true;
try { try {
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
tabUid: this.tab.uid tabUid: props.tab.uid
}; };
await Schema.rollbackTab(params); await Schema.rollbackTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false }); setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'ROLLBACK' }) }); addNotification({ status: 'success', message: t('message.actionSuccessful', { action: 'ROLLBACK' }) });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
this.isQuering = false; isQuering.value = false;
}
}
}; };
query.value = props.tab.content as string;
selectedSchema.value = props.tab.schema || breadcrumbsSchema.value;
if (!databaseSchemas.value.includes(selectedSchema.value))
selectedSchema.value = null;
// window.addEventListener('keydown', onKey);
window.addEventListener('resize', onWindowResize);
onMounted(() => {
const localResizer = resizer.value;
localResizer.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
if (props.tab.autorun)
runQuery(query.value);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize);
// window.removeEventListener('keydown', onKey);
const params = {
uid: props.connection.uid,
tabUid: props.tab.uid
};
Schema.destroyConnectionToCommit(params);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -3,25 +3,25 @@
<div class="columns"> <div class="columns">
<div class="column col-16 text-right"> <div class="column col-16 text-right">
<div class="mb-4"> <div class="mb-4">
{{ $t('message.runQuery') }} {{ t('message.runQuery') }}
</div> </div>
<div v-if="customizations.cancelQueries" class="mb-4"> <div v-if="customizations.cancelQueries" class="mb-4">
{{ $t('message.killQuery') }} {{ t('message.killQuery') }}
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ $t('word.format') }} {{ t('word.format') }}
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ $t('word.clear') }} {{ t('word.clear') }}
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ $t('word.history') }} {{ t('word.history') }}
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ $t('message.openNewTab') }} {{ t('message.openNewTab') }}
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ $t('message.closeTab') }} {{ t('message.closeTab') }}
</div> </div>
</div> </div>
<div class="column col-16"> <div class="column col-16">
@ -51,13 +51,14 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { useI18n } from 'vue-i18n';
name: 'WorkspaceTabQueryEmptyState',
props: { const { t } = useI18n();
defineProps({
customizations: Object customizations: Object
} });
};
</script> </script>
<style scoped> <style scoped>

View File

@ -97,131 +97,120 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> <i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span> <span class="cut-text">{{ t('message.deleteRows', selectedRows.length) }}</span>
</div> </div>
</template> </template>
<template #body> <template #body>
<div class="mb-2"> <div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }} {{ t('message.confirmToDeleteRows', selectedRows.length) }}
</div> </div>
</template> </template>
</ConfirmModal> </ConfirmModal>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, computed, nextTick, onMounted, onUnmounted, onUpdated, Prop, ref, Ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { arrayToFile } from '../libs/arrayToFile'; import { arrayToFile } from '../libs/arrayToFile';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow'; import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue';
import TableContext from '@/components/WorkspaceTabQueryTableContext'; import TableContext from '@/components/WorkspaceTabQueryTableContext.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import moment from 'moment'; import moment from 'moment';
import { useI18n } from 'vue-i18n';
import { TableField, QueryResult } from 'common/interfaces/antares';
import { TableUpdateParams } from 'common/interfaces/tableApis';
const { t } = useI18n();
export default {
name: 'WorkspaceTabQueryTable',
components: {
BaseVirtualScroll,
WorkspaceTabQueryTableRow,
TableContext,
ConfirmModal
},
props: {
results: Array,
connUid: String,
mode: String,
isSelected: Boolean,
elementType: { type: String, default: 'table' }
},
emits: ['update-field', 'delete-selected', 'hard-sort'],
setup () {
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const { getWorkspace } = useWorkspacesStore(); const { getWorkspace } = useWorkspacesStore();
const { dataTabLimit: pageSize } = storeToRefs(settingsStore); const { dataTabLimit: pageSize } = storeToRefs(settingsStore);
return { const props = defineProps({
addNotification, results: Array as Prop<QueryResult[]>,
pageSize, connUid: String,
getWorkspace mode: String,
}; isSelected: Boolean,
}, elementType: { type: String, default: 'table' }
data () { });
return {
resultsSize: 0, const emit = defineEmits(['update-field', 'delete-selected', 'hard-sort']);
localResults: [],
isContext: false, const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
isDeleteConfirmModal: false, const tableWrapper: Ref<HTMLDivElement> = ref(null);
contextEvent: null, const table: Ref<HTMLDivElement> = ref(null);
selectedCell: null, const resultsSize = ref(0);
selectedRows: [], const localResults: Ref<QueryResult<any>[]> = ref([]);
currentSort: '', const isContext = ref(false);
currentSortDir: 'asc', const isDeleteConfirmModal = ref(false);
resultsetIndex: 0, const contextEvent = ref(null);
scrollElement: null, const selectedCell = ref(null);
rowHeight: 23, const selectedRows = ref([]);
selectedField: null, const currentSort = ref('');
isEditingRow: false const currentSortDir = ref('asc');
}; const resultsetIndex = ref(0);
}, const scrollElement = ref(null);
computed: { const rowHeight = ref(23);
workspaceSchema () { const selectedField = ref(null);
return this.getWorkspace(this.connUid).breadcrumbs.schema; const isEditingRow = ref(false);
},
primaryField () { const workspaceSchema = computed(() => getWorkspace(props.connUid).breadcrumbs.schema);
const primaryFields = this.fields.filter(field => field.key === 'pri');
const uniqueFields = this.fields.filter(field => field.key === 'uni'); const primaryField = computed(() => {
const primaryFields = fields.value.filter(field => field.key === 'pri');
const uniqueFields = fields.value.filter(field => field.key === 'uni');
if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length)) if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length))
return false; return null;
return primaryFields[0] || uniqueFields[0]; return primaryFields[0] || uniqueFields[0];
}, });
isSortable () {
return this.fields.every(field => field.name); const isSortable = computed(() => {
}, return fields.value.every(field => field.name);
isHardSort () { });
return this.mode === 'table' && this.localResults.length === this.pageSize;
}, const isHardSort = computed(() => {
sortedResults () { return props.mode === 'table' && localResults.value.length === pageSize.value;
if (this.currentSort && !this.isHardSort) { });
return [...this.localResults].sort((a, b) => {
const sortedResults = computed(() => {
if (currentSort.value && !isHardSort.value) {
return [...localResults.value].sort((a: any, b: any) => {
let modifier = 1; let modifier = 1;
let valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort]; let valA = typeof a[currentSort.value] === 'string' ? a[currentSort.value].toLowerCase() : a[currentSort.value];
if (!isNaN(valA)) valA = Number(valA); if (!isNaN(valA)) valA = Number(valA);
let valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort]; let valB = typeof b[currentSort.value] === 'string' ? b[currentSort.value].toLowerCase() : b[currentSort.value];
if (!isNaN(valB)) valB = Number(valB); if (!isNaN(valB)) valB = Number(valB);
if (this.currentSortDir === 'desc') modifier = -1; if (currentSortDir.value === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier; if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier; if (valA > valB) return 1 * modifier;
return 0; return 0;
}); });
} }
else else
return this.localResults; return localResults.value;
}, });
resultsWithRows () {
return this.results.filter(result => result.rows); const resultsWithRows = computed(() => props.results.filter(result => result.rows));
}, const fields = computed(() => resultsWithRows.value.length ? resultsWithRows.value[resultsetIndex.value].fields : []);
fields () { const keyUsage = computed(() => resultsWithRows.value.length ? resultsWithRows.value[resultsetIndex.value].keys : []);
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].fields : [];
}, const fieldsObj = computed(() => {
keyUsage () { if (sortedResults.value.length) {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : []; const fieldsObj: {[key: string]: TableField} = {};
}, for (const key in sortedResults.value[0]) {
fieldsObj () {
if (this.sortedResults.length) {
const fieldsObj = {};
for (const key in this.sortedResults[0]) {
if (key === '_antares_id') continue; if (key === '_antares_id') continue;
const fieldObj = this.fields.find(field => { const fieldObj = fields.value.find(field => {
let fieldNames = [ let fieldNames = [
field.name, field.name,
field.alias, field.alias,
@ -245,64 +234,16 @@ export default {
return fieldsObj; return fieldsObj;
} }
return {}; return {};
}
},
watch: {
results () {
this.setLocalResults();
this.resultsetIndex = 0;
},
resultsetIndex () {
this.setLocalResults();
},
isSelected (val) {
if (val) this.refreshScroller();
}
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
document.querySelectorAll('.column-resizable').forEach(element => {
if (element.clientWidth !== 0)
element.style.width = element.clientWidth + 'px';
}); });
},
mounted () {
window.addEventListener('resize', this.resizeResults);
window.addEventListener('keydown', this.onKey);
},
unmounted () {
window.removeEventListener('resize', this.resizeResults);
window.removeEventListener('keydown', this.onKey);
},
methods: {
fieldType (cKey) {
let type = 'unknown';
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
type = field.type;
return type; const fieldLength = (field: TableField) => {
},
fieldPrecision (cKey) {
let length = 0;
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
length = field.datePrecision;
return length;
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength; else if (TEXT.includes(field.type)) return field.charLength;
else if (field.numScale) return `${field.numPrecision}, ${field.numScale}`; else if (field.numScale) return `${field.numPrecision}, ${field.numScale}`;
return field.length; return field.length;
}, };
keyName (key) {
const keyName = (key: string) => {
switch (key) { switch (key) {
case 'pri': case 'pri':
return 'PRIMARY'; return 'PRIMARY';
@ -313,57 +254,62 @@ export default {
default: default:
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }
}, };
getTable (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length) const getTable = (index: number) => {
return this.resultsWithRows[index].fields[0].table; if (resultsWithRows.value[index] && resultsWithRows.value[index].fields && resultsWithRows.value[index].fields.length)
return resultsWithRows.value[index].fields[0].table;
return ''; return '';
}, };
getSchema (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length) const getSchema = (index: number) => {
return this.resultsWithRows[index].fields[0].schema; if (resultsWithRows.value[index] && resultsWithRows.value[index].fields && resultsWithRows.value[index].fields.length)
return this.workspaceSchema; return resultsWithRows.value[index].fields[0].schema;
}, return workspaceSchema.value;
getPrimaryValue (row) { };
const getPrimaryValue = (row: any) => {
const primaryFieldName = Object.keys(row).find(prop => [ const primaryFieldName = Object.keys(row).find(prop => [
this.primaryField.alias, primaryField.value.alias,
this.primaryField.name, primaryField.value.name,
`${this.primaryField.table}.${this.primaryField.alias}`, `${primaryField.value.table}.${primaryField.value.alias}`,
`${this.primaryField.table}.${this.primaryField.name}`, `${primaryField.value.table}.${primaryField.value.name}`,
`${this.primaryField.tableAlias}.${this.primaryField.alias}`, `${primaryField.value.tableAlias}.${primaryField.value.alias}`,
`${this.primaryField.tableAlias}.${this.primaryField.name}` `${primaryField.value.tableAlias}.${primaryField.value.name}`
].includes(prop)); ].includes(prop));
return row[primaryFieldName]; return row[primaryFieldName];
}, };
setLocalResults () {
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows const setLocalResults = () => {
? this.resultsWithRows[this.resultsetIndex].rows.map(item => { localResults.value = resultsWithRows.value[resultsetIndex.value] && resultsWithRows.value[resultsetIndex.value].rows
? resultsWithRows.value[resultsetIndex.value].rows.map(item => {
return { ...item, _antares_id: uidGen() }; return { ...item, _antares_id: uidGen() };
}) })
: []; : [];
}, };
resizeResults () {
if (this.$refs.resultTable && this.isSelected) { const resizeResults = () => {
const el = this.$refs.tableWrapper; if (resultTable.value && props.isSelected) {
const el = tableWrapper.value;
if (el) { if (el) {
const footer = document.getElementById('footer'); const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; resultsSize.value = size;
} }
this.$refs.resultTable.updateWindow(); resultTable.value.updateWindow();
} }
}, };
refreshScroller () {
this.resizeResults(); const refreshScroller = () => resizeResults();
},
updateField (payload, row) { const updateField = (payload: { field: string; type: string; content: any }, row: {[key: string]: any}) => {
const orgRow = this.localResults.find(lr => lr._antares_id === row._antares_id); const orgRow: any = localResults.value.find((lr: any) => lr._antares_id === row._antares_id);
Object.keys(orgRow).forEach(key => { // remap the row Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
let datePrecision = ''; let datePrecision = '';
const precision = this.fields.find(field => field.name === key).datePrecision; const precision = fields.value.find(field => field.name === key)?.datePrecision;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
@ -372,80 +318,88 @@ export default {
}); });
const params = { const params = {
primary: this.primaryField.name, primary: primaryField.value.name,
schema: this.getSchema(this.resultsetIndex), schema: getSchema(resultsetIndex.value),
table: this.getTable(this.resultsetIndex), table: getTable(resultsetIndex.value),
id: this.getPrimaryValue(orgRow), id: getPrimaryValue(orgRow),
row, row,
orgRow, orgRow,
...payload ...payload
}; };
this.$emit('update-field', params); emit('update-field', params);
}, };
closeContext () {
this.isContext = false; const closeContext = () => {
}, isContext.value = false;
showDeleteConfirmModal (e) { };
const showDeleteConfirmModal = (e: any) => {
if (e && e.path && ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.path[0].tagName)) if (e && e.path && ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.path[0].tagName))
return; return;
this.isDeleteConfirmModal = true; isDeleteConfirmModal.value = true;
}, };
hideDeleteConfirmModal () {
this.isDeleteConfirmModal = false; const hideDeleteConfirmModal = () => {
}, isDeleteConfirmModal.value = false;
deleteSelected () { };
this.closeContext();
const rows = JSON.parse(JSON.stringify(this.localResults)).filter(row => this.selectedRows.includes(row._antares_id)).map(row => { const deleteSelected = () => {
closeContext();
const rows = JSON.parse(JSON.stringify(localResults.value)).filter((row: any) => selectedRows.value.includes(row._antares_id)).map((row: any) => {
delete row._antares_id; delete row._antares_id;
return row; return row;
}); });
const params = { const params = {
primary: this.primaryField.name, primary: primaryField.value.name,
schema: this.getSchema(this.resultsetIndex), schema: getSchema(resultsetIndex.value),
table: this.getTable(this.resultsetIndex), table: getTable(resultsetIndex.value),
rows rows
}; };
this.$emit('delete-selected', params); emit('delete-selected', params);
}, };
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id)); const setNull = () => {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const params = { const params = {
primary: this.primaryField.name, primary: primaryField.value.name,
schema: this.getSchema(this.resultsetIndex), schema: getSchema(resultsetIndex.value),
table: this.getTable(this.resultsetIndex), table: getTable(resultsetIndex.value),
id: this.getPrimaryValue(row), id: getPrimaryValue(row),
row, row,
orgRow: row, orgRow: row,
field: this.selectedCell.field, field: selectedCell.value.field,
content: null content: null as string
}; };
this.$emit('update-field', params); emit('update-field', params);
}, };
copyCell () {
const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id)); const copyCell = () => {
const row: any = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const cellName = Object.keys(row).find(prop => [ const cellName = Object.keys(row).find(prop => [
this.selectedCell.field, selectedCell.value.field,
this.selectedCell.orgField, selectedCell.value.orgField,
`${this.fields[0].table}.${this.selectedCell.field}`, `${fields.value[0].table}.${selectedCell.value.field}`,
`${this.fields[0].tableAlias}.${this.selectedCell.field}` `${fields.value[0].tableAlias}.${selectedCell.value.field}`
].includes(prop)); ].includes(prop));
let valueToCopy = row[cellName]; let valueToCopy = row[cellName];
if (typeof valueToCopy === 'object') if (typeof valueToCopy === 'object')
valueToCopy = JSON.stringify(valueToCopy); valueToCopy = JSON.stringify(valueToCopy);
navigator.clipboard.writeText(valueToCopy); navigator.clipboard.writeText(valueToCopy);
}, };
copyRow () {
const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id)); const copyRow = () => {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = JSON.parse(JSON.stringify(row));
delete rowToCopy._antares_id; delete rowToCopy._antares_id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); navigator.clipboard.writeText(JSON.stringify(rowToCopy));
}, };
applyUpdate (params) {
const applyUpdate = (params: TableUpdateParams) => {
const { primary, id, field, table, content } = params; const { primary, id, field, table, content } = params;
this.localResults = this.localResults.map(row => { localResults.value = localResults.value.map((row: any) => {
if (row[primary] === id)// only fieldName if (row[primary] === id)// only fieldName
row[field] = content; row[field] = content;
else if (row[`${table}.${primary}`] === id)// table.fieldName else if (row[`${table}.${primary}`] === id)// table.fieldName
@ -453,92 +407,100 @@ export default {
return row; return row;
}); });
}, };
selectRow (event, row, field) {
this.selectedField = field; const selectRow = (event: KeyboardEvent, row: any, field: string) => {
selectedField.value = field;
const selectedRowId = row._antares_id; const selectedRowId = row._antares_id;
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
if (this.selectedRows.includes(selectedRowId)) if (selectedRows.value.includes(selectedRowId))
this.selectedRows = this.selectedRows.filter(el => el !== selectedRowId); selectedRows.value = selectedRows.value.filter(el => el !== selectedRowId);
else else
this.selectedRows.push(selectedRowId); selectedRows.value.push(selectedRowId);
} }
else if (event.shiftKey) { else if (event.shiftKey) {
if (!this.selectedRows.length) if (!selectedRows.value.length)
this.selectedRows.push(selectedRowId); selectedRows.value.push(selectedRowId);
else { else {
const lastID = this.selectedRows.slice(-1)[0]; const lastID = selectedRows.value.slice(-1)[0];
const lastIndex = this.sortedResults.findIndex(el => el._antares_id === lastID); const lastIndex = sortedResults.value.findIndex((el: any) => el._antares_id === lastID);
const clickedIndex = this.sortedResults.findIndex(el => el._antares_id === selectedRowId); const clickedIndex = sortedResults.value.findIndex((el: any) => el._antares_id === selectedRowId);
if (lastIndex > clickedIndex) { if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++) for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.sortedResults[i]._antares_id); selectedRows.value.push((sortedResults.value[i] as any)._antares_id);
} }
else if (lastIndex < clickedIndex) { else if (lastIndex < clickedIndex) {
for (let i = clickedIndex; i > lastIndex; i--) for (let i = clickedIndex; i > lastIndex; i--)
this.selectedRows.push(this.sortedResults[i]._antares_id); selectedRows.value.push((sortedResults.value[i] as any)._antares_id);
} }
} }
} }
else else
this.selectedRows = [selectedRowId]; selectedRows.value = [selectedRowId];
}, };
selectAllRows (e) {
if (e.target.classList.contains('editable-field')) return;
this.selectedField = 0; const selectAllRows = (e: KeyboardEvent) => {
this.selectedRows = this.localResults.reduce((acc, curr) => { if ((e.target as HTMLElement).classList.contains('editable-field')) return;
selectedField.value = 0;
selectedRows.value = localResults.value.reduce((acc, curr: any) => {
acc.push(curr._antares_id); acc.push(curr._antares_id);
return acc; return acc;
}, []); }, []);
}, };
deselectRows () {
if (!this.isEditingRow)
this.selectedRows = [];
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell; const deselectRows = () => {
if (!this.selectedRows.includes(cell.id)) if (!isEditingRow.value)
this.selectedRows = [cell.id]; selectedRows.value = [];
this.contextEvent = event; };
this.isContext = true;
},
sort (field) {
if (!this.isSortable) return;
this.selectedRows = []; const contextMenu = (event: MouseEvent, cell: any) => {
if ((event.target as HTMLElement).localName === 'input') return;
if (this.mode === 'query') selectedCell.value = cell;
field = `${this.getTable(this.resultsetIndex)}.${field}`; if (!selectedRows.value.includes(cell.id))
selectedRows.value = [cell.id];
contextEvent.value = event;
isContext.value = true;
};
if (field === this.currentSort) { const sort = (field: string) => {
if (this.currentSortDir === 'asc') if (!isSortable.value) return;
this.currentSortDir = 'desc';
selectedRows.value = [];
if (props.mode === 'query')
field = `${getTable(resultsetIndex.value)}.${field}`;
if (field === currentSort.value) {
if (currentSortDir.value === 'asc')
currentSortDir.value = 'desc';
else else
this.resetSort(); resetSort();
} }
else { else {
this.currentSortDir = 'asc'; currentSortDir.value = 'asc';
this.currentSort = field; currentSort.value = field;
} }
if (this.isHardSort) if (isHardSort.value)
this.$emit('hard-sort', { field: this.currentSort, dir: this.currentSortDir }); emit('hard-sort', { field: currentSort.value, dir: currentSortDir.value });
}, };
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
selectResultset (index) {
this.resultsetIndex = index;
},
downloadTable (format, filename) {
if (!this.sortedResults) return;
const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => { const resetSort = () => {
currentSort.value = '';
currentSortDir.value = 'asc';
};
const selectResultset = (index: number) => {
resultsetIndex.value = index;
};
const downloadTable = (format: 'csv' | 'json', filename: string) => {
if (!sortedResults.value) return;
const rows = JSON.parse(JSON.stringify(sortedResults.value)).map((row: any) => {
delete row._antares_id; delete row._antares_id;
return row; return row;
}); });
@ -548,29 +510,30 @@ export default {
content: rows, content: rows,
filename filename
}); });
}, };
onKey (e) {
if (!this.isSelected) const onKey = async (e: KeyboardEvent) => {
if (!props.isSelected)
return; return;
if (this.isEditingRow) if (isEditingRow.value)
return; return;
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyA' && !e.altKey) if ((e.ctrlKey || e.metaKey) && e.code === 'KeyA' && !e.altKey)
this.selectAllRows(e); selectAllRows(e);
// row naviation stuff // row navigation stuff
if ((e.code.includes('Arrow') || e.code === 'Tab') && this.sortedResults.length > 0 && !e.altKey) { if ((e.code.includes('Arrow') || e.code === 'Tab') && sortedResults.value.length > 0 && !e.altKey) {
e.preventDefault(); e.preventDefault();
const aviableFields= Object.keys(this.sortedResults[0]).slice(0, -1); // removes _antares_id const aviableFields= Object.keys(sortedResults.value[0]).slice(0, -1); // removes _antares_id
if (!this.selectedField) if (!selectedField.value)
this.selectedField = aviableFields[0]; selectedField.value = aviableFields[0];
const selectedId = this.selectedRows[0]; const selectedId = selectedRows.value[0];
const selectedIndex = this.sortedResults.findIndex(row => row._antares_id === selectedId); const selectedIndex = sortedResults.value.findIndex((row: any) => row._antares_id === selectedId);
const selectedFieldIndex = aviableFields.findIndex(field => field === this.selectedField); const selectedFieldIndex = aviableFields.findIndex(field => field === selectedField.value);
let nextIndex = 0; let nextIndex = 0;
let nextFieldIndex = 0; let nextFieldIndex = 0;
@ -580,8 +543,8 @@ export default {
nextIndex = selectedIndex + 1; nextIndex = selectedIndex + 1;
nextFieldIndex = selectedFieldIndex; nextFieldIndex = selectedFieldIndex;
if (nextIndex > this.sortedResults.length -1) if (nextIndex > sortedResults.value.length -1)
nextIndex = this.sortedResults.length -1; nextIndex = sortedResults.value.length -1;
break; break;
case 'ArrowUp': case 'ArrowUp':
@ -626,38 +589,78 @@ export default {
} }
} }
if (this.sortedResults[nextIndex] && nextIndex !== selectedIndex) { if (sortedResults.value[nextIndex] && nextIndex !== selectedIndex) {
this.selectedRows = [this.sortedResults[nextIndex]._antares_id]; selectedRows.value = [(sortedResults.value[nextIndex] as any)._antares_id];
this.$nextTick(() => this.scrollToCell(this.scrollElement.querySelector('.td.selected'))); await nextTick();
scrollToCell(scrollElement.value.querySelector('.td.selected'));
} }
if (aviableFields[nextFieldIndex] && nextFieldIndex !== selectedFieldIndex) { if (aviableFields[nextFieldIndex] && nextFieldIndex !== selectedFieldIndex) {
this.selectedField = aviableFields[nextFieldIndex]; selectedField.value = aviableFields[nextFieldIndex];
this.$nextTick(() => this.scrollToCell(this.scrollElement.querySelector('.td.selected'))); await nextTick();
} scrollToCell(scrollElement.value.querySelector('.td.selected'));
}
},
scrollToCell (el) {
if (!el) return;
const visYMin = this.scrollElement.scrollTop;
const visYMax = this.scrollElement.scrollTop + this.scrollElement.clientHeight - el.clientHeight;
const visXMin = this.scrollElement.scrollLeft;
const visXMax = this.scrollElement.scrollLeft + this.scrollElement.clientWidth - el.clientWidth;
if (el.offsetTop < visYMin)
this.scrollElement.scrollTop = el.offsetTop;
else if (el.offsetTop >= visYMax)
this.scrollElement.scrollTop = el.offsetTop - this.scrollElement.clientHeight + el.clientHeight;
if (el.offsetLeft < visXMin)
this.scrollElement.scrollLeft = el.offsetLeft;
else if (el.offsetLeft >= visXMax)
this.scrollElement.scrollLeft = el.offsetLeft - this.scrollElement.clientWidth + el.clientWidth;
} }
} }
}; };
const scrollToCell = (el: HTMLElement) => {
if (!el) return;
const visYMin = scrollElement.value.scrollTop;
const visYMax = scrollElement.value.scrollTop + scrollElement.value.clientHeight - el.clientHeight;
const visXMin = scrollElement.value.scrollLeft;
const visXMax = scrollElement.value.scrollLeft + scrollElement.value.clientWidth - el.clientWidth;
if (el.offsetTop < visYMin)
scrollElement.value.scrollTop = el.offsetTop;
else if (el.offsetTop >= visYMax)
scrollElement.value.scrollTop = el.offsetTop - scrollElement.value.clientHeight + el.clientHeight;
if (el.offsetLeft < visXMin)
scrollElement.value.scrollLeft = el.offsetLeft;
else if (el.offsetLeft >= visXMax)
scrollElement.value.scrollLeft = el.offsetLeft - scrollElement.value.clientWidth + el.clientWidth;
};
defineExpose({ applyUpdate, refreshScroller, resetSort, resizeResults, downloadTable });
watch(() => props.results, () => {
setLocalResults();
resultsetIndex.value = 0;
});
watch(resultsetIndex, () => {
setLocalResults();
});
watch(() => props.isSelected, (val) => {
if (val) refreshScroller();
});
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
document.querySelectorAll<HTMLElement>('.column-resizable').forEach(element => {
if (element.clientWidth !== 0)
element.style.width = element.clientWidth + 'px';
});
});
onMounted(() => {
window.addEventListener('resize', resizeResults);
window.addEventListener('keydown', onKey);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeResults);
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
@close-context="closeContext" @close-context="closeContext"
> >
<div v-if="selectedRows.length === 1" class="context-element"> <div v-if="selectedRows.length === 1" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div <div
@ -13,7 +13,7 @@
@click="copyCell" @click="copyCell"
> >
<span class="d-flex"> <span class="d-flex">
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }} <i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ t('word.cell', 1) }}
</span> </span>
</div> </div>
<div <div
@ -22,7 +22,7 @@
@click="copyRow" @click="copyRow"
> >
<span class="d-flex"> <span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }} <i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', 1) }}
</span> </span>
</div> </div>
</div> </div>
@ -33,7 +33,7 @@
@click="setNull" @click="setNull"
> >
<span class="d-flex"> <span class="d-flex">
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }} <i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ t('message.setNull') }}
</span> </span>
</div> </div>
<div <div
@ -42,45 +42,46 @@
@click="showConfirmModal" @click="showConfirmModal"
> >
<span class="d-flex"> <span class="d-flex">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }} <i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ t('message.deleteRows', selectedRows.length) }}
</span> </span>
</div> </div>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceTabQueryTableContext',
components: { defineProps({
BaseContextMenu
},
props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedRows: Array, selectedRows: Array,
selectedCell: Object selectedCell: Object
}, });
emits: ['show-delete-modal', 'close-context', 'set-null', 'copy-cell', 'copy-row'],
methods: { const emit = defineEmits(['show-delete-modal', 'close-context', 'set-null', 'copy-cell', 'copy-row']);
showConfirmModal () {
this.$emit('show-delete-modal'); const showConfirmModal = () => {
}, emit('show-delete-modal');
closeContext () { };
this.$emit('close-context');
}, const closeContext = () => {
setNull () { emit('close-context');
this.$emit('set-null'); };
this.closeContext();
}, const setNull = () => {
copyCell () { emit('set-null');
this.$emit('copy-cell'); closeContext();
this.closeContext(); };
},
copyRow () { const copyCell = () => {
this.$emit('copy-row'); emit('copy-cell');
this.closeContext(); closeContext();
} };
}
const copyRow = () => {
emit('copy-row');
closeContext();
}; };
</script> </script>

View File

@ -19,7 +19,7 @@
class="cell-content" class="cell-content"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`" :class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON(cKey)" @dblclick="editON(cKey)"
>{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length)) }}</span> >{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length) as string) }}</span>
<ForeignKeySelect <ForeignKeySelect
v-else-if="isForeignKey(cKey)" v-else-if="isForeignKey(cKey)"
v-model="editingContent" v-model="editingContent"
@ -68,14 +68,14 @@
</div> </div>
<ConfirmModal <ConfirmModal
v-if="isTextareaEditor" v-if="isTextareaEditor"
:confirm-text="$t('word.update')" :confirm-text="t('word.update')"
size="medium" size="medium"
@confirm="editOFF" @confirm="editOFF"
@hide="hideEditorModal" @hide="hideEditorModal"
> >
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span> <i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ t('word.edit') }} "{{ editingField }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -90,7 +90,7 @@
<div class="editor-field-info p-vcentered"> <div class="editor-field-info p-vcentered">
<div class="d-flex p-vcentered"> <div class="d-flex p-vcentered">
<label for="editorMode" class="form-label mr-2"> <label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>: <b>{{ t('word.content') }}</b>:
</label> </label>
<BaseSelect <BaseSelect
id="editorMode" id="editorMode"
@ -104,10 +104,10 @@
<div class="d-flex"> <div class="d-flex">
<div class="p-vcentered"> <div class="p-vcentered">
<div class="mr-4"> <div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }} <b>{{ t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div> </div>
<div v-if="editingType"> <div v-if="editingType">
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }} <b>{{ t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div> </div>
</div> </div>
</div> </div>
@ -132,14 +132,14 @@
</ConfirmModal> </ConfirmModal>
<ConfirmModal <ConfirmModal
v-if="isBlobEditor" v-if="isBlobEditor"
:confirm-text="$t('word.update')" :confirm-text="t('word.update')"
@confirm="editOFF" @confirm="editOFF"
@hide="hideEditorModal" @hide="hideEditorModal"
> >
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span> <span class="cut-text">{{ t('word.edit') }} "{{ editingField }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -156,11 +156,11 @@
</div> </div>
<div class="editor-buttons mt-2"> <div class="editor-buttons mt-2">
<button class="btn btn-link btn-sm" @click="downloadFile"> <button class="btn btn-link btn-sm" @click="downloadFile">
<span>{{ $t('word.download') }}</span> <span>{{ t('word.download') }}</span>
<i class="mdi mdi-24px mdi-download ml-1" /> <i class="mdi mdi-24px mdi-download ml-1" />
</button> </button>
<button class="btn btn-link btn-sm" @click="prepareToDelete"> <button class="btn btn-link btn-sm" @click="prepareToDelete">
<span>{{ $t('word.delete') }}</span> <span>{{ t('word.delete') }}</span>
<i class="mdi mdi-24px mdi-delete-forever ml-1" /> <i class="mdi mdi-24px mdi-delete-forever ml-1" />
</button> </button>
</div> </div>
@ -168,19 +168,19 @@
</Transition> </Transition>
<div class="editor-field-info"> <div class="editor-field-info">
<div> <div>
<b>{{ $t('word.size') }}</b>: {{ formatBytes(editingContent.length) }}<br> <b>{{ t('word.size') }}</b>: {{ formatBytes(editingContent.length) }}<br>
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }} <b>{{ t('word.mimeType') }}</b>: {{ contentInfo.mime }}
</div> </div>
<div v-if="editingType"> <div v-if="editingType">
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }} <b>{{ t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label>{{ $t('message.uploadFile') }}</label> <label>{{ t('message.uploadFile') }}</label>
<input <input
class="form-input" class="form-input"
type="file" type="file"
@change="filesChange($event)" @change="filesChange($event as any)"
> >
</div> </div>
</div> </div>
@ -189,13 +189,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { computed, onBeforeUnmount, Prop, ref, Ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { ModelOperations } from '@vscode/vscode-languagedetection'; import { ModelOperations } from '@vscode/vscode-languagedetection';
import { mimeFromHex } from 'common/libs/mimeFromHex'; import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64'; import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import { import {
TEXT, TEXT,
LONG_TEXT, LONG_TEXT,
@ -213,52 +215,49 @@ import {
SPATIAL, SPATIAL,
IS_MULTI_SPATIAL IS_MULTI_SPATIAL
} from 'common/fieldTypes'; } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor'; import TextEditor from '@/components/BaseTextEditor.vue';
import BaseMap from '@/components/BaseMap'; import BaseMap from '@/components/BaseMap.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { QueryForeign, TableField } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabQueryTableRow',
components: { const props = defineProps({
ConfirmModal,
TextEditor,
ForeignKeySelect,
BaseMap,
BaseSelect
},
props: {
row: Object, row: Object,
fields: Object, fields: Object as Prop<{
keyUsage: Array, [key: string]: TableField;
}>,
keyUsage: Array as Prop<QueryForeign[]>,
itemHeight: Number, itemHeight: Number,
elementType: { type: String, default: 'table' }, elementType: { type: String, default: 'table' },
selected: { type: Boolean, default: false }, selected: { type: Boolean, default: false },
selectedCell: { type: String, default: null } selectedCell: { type: String, default: null }
}, });
emits: ['update-field', 'select-row', 'contextmenu', 'start-editing', 'stop-editing'],
data () { const emit = defineEmits(['update-field', 'select-row', 'contextmenu', 'start-editing', 'stop-editing']);
return {
isInlineEditor: {}, // eslint-disable-next-line @typescript-eslint/no-explicit-any
isTextareaEditor: false, const isInlineEditor: Ref<any> = ref({});
isBlobEditor: false, const isTextareaEditor = ref(false);
isMapModal: false, const isBlobEditor = ref(false);
isMultiSpatial: false, const isMapModal = ref(false);
willBeDeleted: false, const isMultiSpatial = ref(false);
originalContent: null, const willBeDeleted = ref(false);
editingContent: null, const originalContent = ref(null);
editingType: null, const editingContent = ref(null);
editingField: null, const editingType = ref(null);
editingLength: null, const editingField = ref(null);
editorMode: 'text', const editingLength = ref(null);
contentInfo: { const editorMode = ref('text');
const contentInfo = ref({
ext: '', ext: '',
mime: '', mime: '',
size: null size: null
}, });
fileToUpload: null, const fileToUpload = ref(null);
availableLanguages: [ const availableLanguages = ref([
{ name: 'TEXT', slug: 'text', id: 'text' }, { name: 'TEXT', slug: 'text', id: 'text' },
{ name: 'HTML', slug: 'html', id: 'html' }, { name: 'HTML', slug: 'html', id: 'html' },
{ name: 'XML', slug: 'xml', id: 'xml' }, { name: 'XML', slug: 'xml', id: 'xml' },
@ -267,313 +266,291 @@ export default {
{ name: 'INI', slug: 'ini', id: 'ini' }, { name: 'INI', slug: 'ini', id: 'ini' },
{ name: 'MARKDOWN', slug: 'markdown', id: 'md' }, { name: 'MARKDOWN', slug: 'markdown', id: 'md' },
{ name: 'YAML', slug: 'yaml', id: 'yaml' } { name: 'YAML', slug: 'yaml', id: 'yaml' }
] ]);
};
}, const inputProps = computed(() => {
computed: { if ([...TEXT, ...LONG_TEXT].includes(editingType.value))
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false }; return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(this.editingType)) if ([...NUMBER, ...FLOAT].includes(editingType.value))
return { type: 'number', mask: false }; return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) { if (TIME.includes(editingType.value)) {
let timeMask = '##:##:##'; let timeMask = '##:##:##';
const precision = this.fields[this.editingField].length; const precision = props.fields[editingField.value].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#'; timeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType)) if (HAS_TIMEZONE.includes(editingType.value))
timeMask += 'X##'; timeMask += 'X##';
return { type: 'text', mask: timeMask }; return { type: 'text', mask: timeMask };
} }
if (DATE.includes(this.editingType)) if (DATE.includes(editingType.value))
return { type: 'text', mask: '####-##-##' }; return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(this.editingType)) { if (DATETIME.includes(editingType.value)) {
let datetimeMask = '####-##-## ##:##:##'; let datetimeMask = '####-##-## ##:##:##';
const precision = this.fields[this.editingField].length; const precision = props.fields[editingField.value].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#'; datetimeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType)) if (HAS_TIMEZONE.includes(editingType.value))
datetimeMask += 'X##'; datetimeMask += 'X##';
return { type: 'text', mask: datetimeMask }; return { type: 'text', mask: datetimeMask };
} }
if (BLOB.includes(this.editingType)) if (BLOB.includes(editingType.value))
return { type: 'file', mask: false }; return { type: 'file', mask: false };
if (BOOLEAN.includes(this.editingType)) if (BOOLEAN.includes(editingType.value))
return { type: 'boolean', mask: false }; return { type: 'boolean', mask: false };
if (SPATIAL.includes(this.editingType)) if (SPATIAL.includes(editingType.value))
return { type: 'map', mask: false }; return { type: 'map', mask: false };
return { type: 'text', mask: false }; return { type: 'text', mask: false };
}, });
isImage () {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
},
isEditable () {
if (this.elementType === 'view') return false;
if (this.fields) { const isImage = computed(() => {
const nElements = Object.keys(this.fields).reduce((acc, curr) => { return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(contentInfo.value.ext);
acc.add(this.fields[curr].table); });
acc.add(this.fields[curr].schema);
const foreignKeys = computed(() => {
return props.keyUsage.map(key => key.field);
});
const isEditable = computed(() => {
if (props.elementType === 'view') return false;
if (props.fields) {
const nElements = Object.keys(props.fields).reduce((acc, curr) => {
acc.add(props.fields[curr].table);
acc.add(props.fields[curr].schema);
return acc; return acc;
}, new Set([])); }, new Set([]));
if (nElements.size > 2) return false; if (nElements.size > 2) return false;
return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table); return !!(props.fields[Object.keys(props.fields)[0]].schema && props.fields[Object.keys(props.fields)[0]].table);
} }
return false; return false;
},
isBaseSelectField () {
return this.isForeignKey(this.editingField) || this.inputProps.type === 'boolean' || this.enumArray;
},
enumArray () {
if (this.fields[this.editingField] && this.fields[this.editingField].enumValues)
return this.fields[this.editingField].enumValues.replaceAll('\'', '').split(',');
return false;
}
},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
}); });
},
isTextareaEditor (val) {
if (val) {
const modelOperations = new ModelOperations();
(async () => {
const detected = await modelOperations.runModel(this.editingContent);
const filteredLanguages = detected.filter(dLang =>
this.availableLanguages.some(aLang => aLang.id === dLang.languageId) &&
dLang.confidence > 0.1
);
if (filteredLanguages.length) const isBaseSelectField = computed(() => {
this.editorMode = this.availableLanguages.find(lang => lang.id === filteredLanguages[0].languageId).slug; return isForeignKey(editingField.value) || inputProps.value.type === 'boolean' || enumArray.value;
})(); });
}
},
selected (isSelected) {
if (isSelected)
window.addEventListener('keydown', this.onKey);
else { const enumArray = computed(() => {
this.editOFF(); if (props.fields[editingField.value] && props.fields[editingField.value].enumValues)
window.removeEventListener('keydown', this.onKey); return props.fields[editingField.value].enumValues.replaceAll('\'', '').split(',');
} return false;
} });
},
beforeUnmount () { const isForeignKey = (key: string) => {
if (this.selected) if (key) {
window.removeEventListener('keydown', this.onKey);
},
methods: {
isForeignKey (key) {
if (key.includes('.')) if (key.includes('.'))
key = key.split('.').pop(); key = key.split('.').pop();
return this.foreignKeys.includes(key); return foreignKeys.value.includes(key);
}, }
isNull (value) { };
const isNull = (value: null | string | number) => {
return value === null ? ' is-null' : ''; return value === null ? ' is-null' : '';
}, };
typeClass (type) {
const typeClass = (type: string) => {
if (type) if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return ''; return '';
}, };
bufferToBase64 (val) {
return bufferToBase64(val);
},
editON (field) {
if (!this.isEditable || this.editingType === 'none') return;
const content = this.row[field]; const editON = async (field: string) => {
const type = this.fields[field].type.toUpperCase(); if (!isEditable.value || editingType.value === 'none') return;
this.originalContent = this.typeFormat(content, type, this.fields[field].length);
this.editingType = type; const content = props.row[field];
this.editingField = field; const type = props.fields[field].type.toUpperCase();
this.editingLength = this.fields[field].length; originalContent.value = typeFormat(content, type, props.fields[field].length);
editingType.value = type;
editingField.value = field;
editingLength.value = props.fields[field].length;
if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) { if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true; isTextareaEditor.value = true;
this.editingContent = this.typeFormat(content, type); editingContent.value = typeFormat(content, type);
this.$emit('start-editing', field); emit('start-editing', field);
return; return;
} }
if (SPATIAL.includes(type)) { if (SPATIAL.includes(type)) {
if (content) { if (content) {
this.isMultiSpatial = IS_MULTI_SPATIAL.includes(type); isMultiSpatial.value = IS_MULTI_SPATIAL.includes(type);
this.isMapModal = true; isMapModal.value = true;
this.editingContent = this.typeFormat(content, type); editingContent.value = typeFormat(content, type);
} }
this.$emit('start-editing', field); emit('start-editing', field);
return; return;
} }
if (BLOB.includes(type)) { if (BLOB.includes(type)) {
this.isBlobEditor = true; isBlobEditor.value = true;
this.editingContent = content || ''; editingContent.value = content || '';
this.fileToUpload = null; fileToUpload.value = null;
this.willBeDeleted = false; willBeDeleted.value = false;
if (content !== null) { if (content !== null) {
const buff = Buffer.from(this.editingContent); const buff = Buffer.from(editingContent.value);
if (buff.length) { if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase(); const hex = buff.toString('hex').substring(0, 8).toUpperCase();
const { ext, mime } = mimeFromHex(hex); const { ext, mime } = mimeFromHex(hex);
this.contentInfo = { contentInfo.value = {
ext, ext,
mime, mime,
size: this.editingContent.length size: editingContent.value.length
}; };
} }
} }
this.$emit('start-editing', field); emit('start-editing', field);
return; return;
} }
// Inline editable fields // Inline editable fields
this.editingContent = this.originalContent; editingContent.value = originalContent.value;
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; isInlineEditor.value = { ...isInlineEditor.value, ...obj };
nextTick(() => {
this.$nextTick(() => { // Focus on input document.querySelector<HTMLInputElement>('.editable-field').focus();
document.querySelector('.editable-field').focus();
}); });
this.$emit('start-editing', field); emit('start-editing', field);
}, };
editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false; const editOFF = () => {
if (!editingField.value) return;
isInlineEditor.value[editingField.value] = false;
let content; let content;
if (!BLOB.includes(this.editingType)) { if (!BLOB.includes(editingType.value)) {
if ([...DATETIME, ...TIME].includes(this.editingType)) { if ([...DATETIME, ...TIME].includes(editingType.value)) {
if (this.editingContent.substring(this.editingContent.length - 1) === '.') if (editingContent.value.substring(editingContent.value.length - 1) === '.')
this.editingContent = this.editingContent.slice(0, -1); editingContent.value = editingContent.value.slice(0, -1);
} }
// If not changed // If not changed
if (this.editingContent === this.typeFormat(this.originalContent, this.editingType, this.editingLength)) { if (editingContent.value === typeFormat(originalContent.value, editingType.value, editingLength.value)) {
this.editingType = null; editingType.value = null;
this.editingField = null; editingField.value = null;
this.$emit('stop-editing', this.editingField); emit('stop-editing', editingField.value);
return; return;
} }
content = this.editingContent; content = editingContent.value;
} }
else { // Handle file upload else { // Handle file upload
if (this.willBeDeleted) { if (willBeDeleted.value) {
content = ''; content = '';
this.willBeDeleted = false; willBeDeleted.value = false;
} }
else { else {
if (!this.fileToUpload) return; if (!fileToUpload.value) return;
content = this.fileToUpload.file.path; content = fileToUpload.value.file.path;
} }
} }
this.$emit('update-field', { emit('update-field', {
field: this.fields[this.editingField].name, field: props.fields[editingField.value].name,
type: this.editingType, type: editingType.value,
content content
}); });
this.$emit('stop-editing', this.editingField); emit('stop-editing', editingField.value);
this.editingType = null; editingType.value = null;
this.editingField = null; editingField.value = null;
}, };
hideEditorModal () {
this.isTextareaEditor = false; const hideEditorModal = () => {
this.isBlobEditor = false; isTextareaEditor.value = false;
this.isMapModal = false; isBlobEditor.value = false;
this.isMultiSpatial = false; isMapModal.value = false;
this.$emit('stop-editing', this.editingField); isMultiSpatial.value = false;
}, emit('stop-editing', editingField.value);
downloadFile () { };
const downloadFile = () => {
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.editingContent)}`; downloadLink.href = `data:${contentInfo.value.mime};base64, ${bufferToBase64(editingContent.value)}`;
downloadLink.setAttribute('download', `${this.editingField}.${this.contentInfo.ext}`); downloadLink.setAttribute('download', `${editingField.value}.${contentInfo.value.ext}`);
document.body.appendChild(downloadLink); document.body.appendChild(downloadLink);
downloadLink.click(); downloadLink.click();
downloadLink.remove(); downloadLink.remove();
}, };
filesChange (event) {
const filesChange = (event: Event & {target: {files: {name: string}[]}}) => {
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;
this.fileToUpload = { name: files[0].name, file: files[0] }; fileToUpload.value = { name: files[0].name, file: files[0] };
this.willBeDeleted = false; willBeDeleted.value = false;
}, };
prepareToDelete () {
this.editingContent = ''; const prepareToDelete = () => {
this.contentInfo = { editingContent.value = '';
contentInfo.value = {
ext: '', ext: '',
mime: '', mime: '',
size: null size: null
}; };
this.willBeDeleted = true; willBeDeleted.value = true;
}, };
selectRow (event, field) {
this.$emit('select-row', event, this.row, field); const selectRow = (event: Event, field: string) => {
}, emit('select-row', event, props.row, field);
getKeyUsage (keyName) { };
const getKeyUsage = (keyName: string) => {
if (keyName.includes('.')) if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop()); return props.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName); return props.keyUsage.find(key => key.field === keyName);
}, };
openContext (event, payload) {
payload.field = this.fields[payload.orgField].name;// Ensures field name only const openContext = (event: MouseEvent, payload: { id: string; field?: string; orgField: string; isEditable?: boolean }) => {
payload.isEditable = this.isEditable; payload.field = props.fields[payload.orgField].name;// Ensures field name only
this.$emit('contextmenu', event, payload); payload.isEditable = isEditable.value;
}, emit('contextmenu', event, payload);
onKey (e) { };
const onKey = (e: KeyboardEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!this.editingField && e.key === 'Enter') if (!editingField.value && e.key === 'Enter')
return this.editON(this.selectedCell); return editON(props.selectedCell);
if (this.editingField && e.key === 'Enter' && !this.isBaseSelectField) if (editingField.value && e.key === 'Enter' && !isBaseSelectField.value)
return this.editOFF(); return editOFF();
if (this.editingField && e.key === 'Escape') { if (editingField.value && e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false; isInlineEditor.value[editingField.value] = false;
this.editingField = null; editingField.value = null;
this.$emit('stop-editing', this.editingField); emit('stop-editing', editingField.value);
} }
}, };
formatBytes,
cutText (val) { const cutText = (val: string) => {
if (typeof val !== 'string') return val; if (typeof val !== 'string') return val;
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val; return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
}, };
typeFormat (val, type, precision) {
const typeFormat = (val: string | number | Date | number[], type: string, precision?: number | false) => {
if (!val) return val; if (!val) return val;
type = type.toUpperCase(); type = type.toUpperCase();
@ -593,7 +570,7 @@ export default {
} }
if (BLOB.includes(type)) { if (BLOB.includes(type)) {
const buff = Buffer.from(val); const buff = Buffer.from(val as string);
if (!buff.length) return ''; if (!buff.length) return '';
const hex = buff.toString('hex').substring(0, 8).toUpperCase(); const hex = buff.toString('hex').substring(0, 8).toUpperCase();
@ -601,10 +578,10 @@ export default {
} }
if (BIT.includes(type)) { if (BIT.includes(type)) {
if (typeof val === 'number') val = [val]; if (typeof val === 'number') val = [val] as number[];
const hex = Buffer.from(val).toString('hex'); const hex = Buffer.from(val as number[]).toString('hex') as unknown as HexChar[];
const bitString = hexToBinary(hex); const bitString = hexToBinary(hex);
return parseInt(bitString).toString().padStart(precision, '0'); return parseInt(bitString).toString().padStart(Number(precision), '0');
} }
if (ARRAY.includes(type)) { if (ARRAY.includes(type)) {
@ -617,9 +594,44 @@ export default {
return val; return val;
return typeof val === 'object' ? JSON.stringify(val) : val; return typeof val === 'object' ? JSON.stringify(val) : val;
}
}
}; };
watch(() => props.fields, () => {
Object.keys(props.fields).forEach(field => {
isInlineEditor.value[field] = false;
});
});
watch(isTextareaEditor, (val) => {
if (val) {
const modelOperations = new ModelOperations();
(async () => {
const detected = await modelOperations.runModel(editingContent.value);
const filteredLanguages = detected.filter(dLang =>
availableLanguages.value.some(aLang => aLang.id === dLang.languageId) &&
dLang.confidence > 0.1
);
if (filteredLanguages.length)
editorMode.value = availableLanguages.value.find(lang => lang.id === filteredLanguages[0].languageId).slug;
})();
}
});
watch(() => props.selected, (isSelected) => {
if (isSelected)
window.addEventListener('keydown', onKey);
else {
editOFF();
window.removeEventListener('keydown', onKey);
}
});
onBeforeUnmount(() => {
if (props.selected)
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -8,7 +8,7 @@
<button <button
class="btn btn-dark btn-sm mr-0 pr-1" class="btn btn-dark btn-sm mr-0 pr-1"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
:title="`${$t('word.refresh')} (F5)`" :title="`${t('word.refresh')} (F5)`"
@click="reloadTable" @click="reloadTable"
> >
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" /> <i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
@ -18,7 +18,7 @@
<i class="mdi mdi-24px mdi-menu-down" /> <i class="mdi mdi-24px mdi-menu-down" />
</div> </div>
<div class="menu px-3"> <div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span> <span>{{ t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input <input
v-model="autorefreshTimer" v-model="autorefreshTimer"
class="slider no-border" class="slider no-border"
@ -46,7 +46,7 @@
{{ page }} {{ page }}
</div> </div>
<div class="menu px-3"> <div class="menu px-3">
<span>{{ $t('message.pageNumber') }}</span> <span>{{ t('message.pageNumber') }}</span>
<input <input
ref="pageSelect" ref="pageSelect"
v-model="pageProxy" v-model="pageProxy"
@ -72,7 +72,7 @@
<button <button
class="btn btn-sm" class="btn btn-sm"
:title="`${$t('word.filter')} (CTRL+F)`" :title="`${t('word.filter')} (CTRL+F)`"
:class="{'btn-primary': isSearch, 'btn-dark': !isSearch}" :class="{'btn-primary': isSearch, 'btn-dark': !isSearch}"
@click="isSearch = !isSearch" @click="isSearch = !isSearch"
> >
@ -95,7 +95,7 @@
tabindex="0" tabindex="0"
> >
<i class="mdi mdi-24px mdi-file-export mr-1" /> <i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span> <span>{{ t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" /> <i class="mdi mdi-24px mdi-menu-down" />
</button> </button>
<ul class="menu text-left"> <ul class="menu text-left">
@ -112,22 +112,22 @@
<div <div
v-if="results.length" v-if="results.length"
class="d-flex" class="d-flex"
:title="$t('message.queryDuration')" :title="t('message.queryDuration')"
> >
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b> <i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div> </div>
<div v-if="results.length && results[0].rows"> <div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ localeString(results[0].rows.length) }}</b> {{ t('word.results') }}: <b>{{ localeString(results[0].rows.length) }}</b>
</div> </div>
<div v-if="hasApproximately || (page > 1 && approximateCount)"> <div v-if="hasApproximately || (page > 1 && approximateCount)">
{{ $t('word.total') }}: <b {{ t('word.total') }}: <b
:title="!customizations.tableRealCount ? $t('word.approximately') : ''" :title="!customizations.tableRealCount ? t('word.approximately') : ''"
> >
<span v-if="!customizations.tableRealCount"></span> <span v-if="!customizations.tableRealCount"></span>
{{ localeString(approximateCount) }} {{ localeString(approximateCount) }}
</b> </b>
</div> </div>
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -167,291 +167,288 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Prop, ref, Ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { useResultTables } from '@/composables/useResultTables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import WorkspaceTabTableFilters from '@/components/WorkspaceTabTableFilters'; import WorkspaceTabTableFilters from '@/components/WorkspaceTabTableFilters.vue';
import ModalFakerRows from '@/components/ModalFakerRows'; import ModalFakerRows from '@/components/ModalFakerRows.vue';
import tableTabs from '@/mixins/tableTabs'; import { ConnectionParams } from 'common/interfaces/antares';
import { TableFilterClausole } from 'common/interfaces/tableApis';
export default { const { t } = useI18n();
name: 'WorkspaceTabTable',
components: { const props = defineProps({
BaseLoader, connection: Object as Prop<ConnectionParams>,
WorkspaceTabQueryTable,
WorkspaceTabTableFilters,
ModalFakerRows
},
mixins: [tableTabs],
props: {
connection: Object,
isSelected: Boolean, isSelected: Boolean,
table: String, table: String,
schema: String, schema: String,
elementType: String elementType: String
}, });
setup () {
const reloadTable = () => getTableData();
const {
queryTable,
isQuering,
updateField,
deleteSelected
} = useResultTables(props.connection.uid, reloadTable);
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { dataTabLimit: limit } = storeToRefs(settingsStore); const { dataTabLimit: limit } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { changeBreadcrumbs, getWorkspace } = workspacesStore; const { changeBreadcrumbs, getWorkspace } = workspacesStore;
return { const pageSelect: Ref<HTMLInputElement> = ref(null);
addNotification, const tabUid = ref('data');
limit, const isPageMenu = ref(false);
selectedWorkspace, const isSearch = ref(false);
changeBreadcrumbs, const results = ref([]);
getWorkspace const lastTable = ref(null);
}; const isFakerModal = ref(false);
}, const autorefreshTimer = ref(0);
data () { const refreshInterval = ref(null);
return { const sortParams = ref({} as { field: string; dir: 'asc' | 'desc'});
tabUid: 'data', // ??? const filters = ref([]);
isQuering: false, const page = ref(1);
isPageMenu: false, const pageProxy = ref(1);
isSearch: false, const approximateCount = ref(0);
results: [],
lastTable: null,
isFakerModal: false,
autorefreshTimer: 0,
refreshInterval: null,
sortParams: {},
filters: [],
page: 1,
pageProxy: 1,
approximateCount: 0
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isTable () {
return !!this.workspace.breadcrumbs.table;
},
fields () {
return this.results.length ? this.results[0].fields : [];
},
keyUsage () {
return this.results.length ? this.results[0].keys : [];
},
tableInfo () {
try {
return this.workspace.structure.find(db => db.name === this.schema).tables.find(table => table.name === this.table);
}
catch (err) {
return { rows: 0 };
}
},
hasApproximately () {
return this.results.length &&
this.results[0].rows &&
this.results[0].rows.length === this.limit &&
this.results[0].rows.length < this.approximateCount;
}
},
watch: {
schema () {
if (this.isSelected) {
this.page = 1;
this.approximateCount = 0;
this.sortParams = {};
this.getTableData();
this.lastTable = this.table;
this.$refs.queryTable.resetSort();
}
},
table () {
if (this.isSelected) {
this.page = 1;
this.approximateCount = 0;
this.sortParams = {};
this.getTableData();
this.lastTable = this.table;
this.$refs.queryTable.resetSort();
}
},
page (val, oldVal) {
if (val && val > 0 && val !== oldVal) {
this.pageProxy = this.page;
this.getTableData();
}
},
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, [this.elementType]: this.table });
if (this.lastTable !== this.table) const workspace = computed(() => {
this.getTableData(); return getWorkspace(props.connection.uid);
} });
},
isSearch (val) { const customizations = computed(() => {
if (this.filters.length > 0 && !val) { return workspace.value.customizations;
this.filters = []; });
this.getTableData();
} const isTable = computed(() => {
this.resizeScroller(); return !!workspace.value.breadcrumbs.table;
} });
},
created () { const fields = computed(() => {
this.getTableData(); return results.value.length ? results.value[0].fields : [];
window.addEventListener('keydown', this.onKey); });
},
beforeUnmount () { const keyUsage = computed(() => {
window.removeEventListener('keydown', this.onKey); return results.value.length ? results.value[0].keys : [];
clearInterval(this.refreshInterval); });
},
methods: { const getTableData = async () => {
async getTableData () { if (!props.table || !props.isSelected) return;
if (!this.table || !this.isSelected) return; isQuering.value = true;
this.isQuering = true;
// if table changes clear cached values // if table changes clear cached values
if (this.lastTable !== this.table) if (lastTable.value !== props.table)
this.results = []; results.value = [];
this.lastTable = this.table; lastTable.value = props.table;
const params = { const params = {
uid: this.connection.uid, uid: props.connection.uid,
schema: this.schema, schema: props.schema,
table: this.table, table: props.table,
limit: this.limit, limit: limit.value,
page: this.page, page: page.value,
sortParams: { ...this.sortParams }, sortParams: { ...sortParams.value },
where: [...this.filters] || [] where: [...filters.value] || []
}; };
try { // Table data try { // Table data
const { status, response } = await Tables.getTableData(params); const { status, response } = await Tables.getTableData(params);
if (status === 'success') if (status === 'success')
this.results = [response]; results.value = [response];
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
if (this.results.length && this.results[0].rows.length === this.limit) { if (results.value.length && results.value[0].rows.length === limit.value) {
try { // Table approximate count try { // Table approximate count
const { status, response } = await Tables.getTableApproximateCount(params); const { status, response } = await Tables.getTableApproximateCount(params);
if (status === 'success') if (status === 'success')
this.approximateCount = response; approximateCount.value = response;
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response as unknown as string });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
} }
} }
this.isQuering = false; isQuering.value = false;
}, };
getTable () {
return this.table;
},
reloadTable () {
this.getTableData();
},
hardSort (sortParams) {
this.sortParams = sortParams;
this.getTableData();
},
openPageMenu () {
if (this.isQuering || (this.results.length && this.results[0].rows.length < this.limit && this.page === 1)) return;
this.isPageMenu = true; const hardSort = (params: { field: string; dir: 'asc' | 'desc'}) => {
if (this.isPageMenu) sortParams.value = params;
setTimeout(() => this.$refs.pageSelect.focus(), 20); getTableData();
}, };
setPageNumber () {
this.isPageMenu = false;
if (this.pageProxy > 0) const openPageMenu = () => {
this.page = this.pageProxy; if (isQuering.value || (results.value.length && results.value[0].rows.length < limit.value && page.value === 1)) return;
isPageMenu.value = true;
if (isPageMenu.value)
setTimeout(() => pageSelect.value.focus(), 20);
};
const setPageNumber = () => {
isPageMenu.value = false;
if (pageProxy.value > 0)
page.value = pageProxy.value;
else else
this.pageProxy = this.page; pageProxy.value = page.value;
}, };
pageChange (direction) {
if (this.isQuering) return;
if (direction === 'next' && (this.results.length && this.results[0].rows.length === this.limit)) { const pageChange = (direction: 'prev' | 'next') => {
if (!this.page) if (isQuering.value) return;
this.page = 2;
if (direction === 'next' && (results.value.length && results.value[0].rows.length === limit.value)) {
if (!page.value)
page.value = 2;
else else
this.page++; page.value++;
} }
else if (direction === 'prev' && this.page > 1) else if (direction === 'prev' && page.value > 1)
this.page--; page.value--;
}, };
showFakerModal () {
if (this.isQuering) return; const showFakerModal = () => {
this.isFakerModal = true; if (isQuering.value) return;
}, isFakerModal.value = true;
hideFakerModal () { };
this.isFakerModal = false;
}, const hideFakerModal = () => {
onKey (e) { isFakerModal.value = false;
if (this.isSelected) { };
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'F5') if (e.key === 'F5')
this.reloadTable(); reloadTable();
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
if (e.key === 'ArrowRight') if (e.key === 'ArrowRight')
this.pageChange('next'); pageChange('next');
if (e.key === 'ArrowLeft') if (e.key === 'ArrowLeft')
this.pageChange('prev'); pageChange('prev');
if (e.keyCode === 70) // f if (e.key === 'f') // f
this.isSearch = !this.isSearch; isSearch.value = !isSearch.value;
}
}
},
setRefreshInterval () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.reloadTable();
}, this.autorefreshTimer * 1000);
}
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table);
},
onFilterChange (clausoles) {
this.resizeScroller();
if (clausoles.length === 0)
this.isSearch = false;
},
resizeScroller () {
setTimeout(() => this.$refs.queryTable.refreshScroller(), 1);
},
updateFilters (clausoles) {
this.filters = clausoles;
this.getTableData();
},
localeString (val) {
if (val !== null)
return val.toLocaleString();
} }
} }
}; };
const setRefreshInterval = () => {
if (refreshInterval.value)
clearInterval(refreshInterval.value);
if (+autorefreshTimer.value) {
refreshInterval.value = setInterval(() => {
if (!isQuering.value)
reloadTable();
}, autorefreshTimer.value * 1000);
}
};
const downloadTable = (format: 'csv' | 'json') => {
queryTable.value.downloadTable(format, props.table);
};
const onFilterChange = (clausoles: TableFilterClausole[]) => {
resizeScroller();
if (clausoles.length === 0)
isSearch.value = false;
};
const resizeScroller = () => {
setTimeout(() => queryTable.value.refreshScroller(), 1);
};
const updateFilters = (clausoles: TableFilterClausole[]) => {
filters.value = clausoles;
getTableData();
};
const localeString = (val: number | null) => {
if (val !== null)
return val.toLocaleString();
};
const hasApproximately = computed(() => {
return results.value.length &&
results.value[0].rows &&
results.value[0].rows.length === limit.value &&
results.value[0].rows.length < approximateCount.value;
});
watch(() => props.schema, () => {
if (props.isSelected) {
page.value = 1;
approximateCount.value = 0;
sortParams.value = {} as { field: string; dir: 'asc' | 'desc'};
getTableData();
lastTable.value = props.table;
queryTable.value.resetSort();
}
});
watch(() => props.table, () => {
if (props.isSelected) {
page.value = 1;
approximateCount.value = 0;
sortParams.value = {} as { field: string; dir: 'asc' | 'desc'};
getTableData();
lastTable.value = props.table;
queryTable.value.resetSort();
}
});
watch(page, (val, oldVal) => {
if (val && val > 0 && val !== oldVal) {
pageProxy.value = page.value;
getTableData();
}
});
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, [props.elementType]: props.table });
if (lastTable.value !== props.table)
getTableData();
}
});
watch(isSearch, (val) => {
if (filters.value.length > 0 && !val) {
filters.value = [];
getTableData();
}
resizeScroller();
});
getTableData();
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
clearInterval(refreshInterval.value);
});
</script> </script>

View File

@ -64,53 +64,47 @@
</form> </form>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, ref } from 'vue';
import { ClientCode, TableField } from 'common/interfaces/antares';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import { NUMBER, FLOAT } from 'common/fieldTypes'; import { NUMBER, FLOAT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { TableFilterClausole } from 'common/interfaces/tableApis';
export default { const props = defineProps({
components: { fields: Array as Prop<TableField[]>,
BaseSelect connClient: String as Prop<ClientCode>
}, });
props: {
fields: Array, const emit = defineEmits(['filter-change', 'filter']);
connClient: String
}, const rows = ref([]);
emits: ['filter-change', 'filter'], const operators = ref([
data () {
return {
rows: [],
operators: [
'=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE', 'BETWEEN', 'IS NULL', 'IS NOT NULL' '=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE', 'BETWEEN', 'IS NULL', 'IS NOT NULL'
] ]);
const clientCustomizations = computed(() => customizations[props.connClient]);
const addRow = () => {
rows.value.push({ active: true, field: props.fields[0].name, op: '=', value: '', value2: '' });
emit('filter-change', rows.value);
}; };
},
computed: { const removeRow = (i: number) => {
customizations () { rows.value = rows.value.filter((_, idx) => idx !== i);
return customizations[this.connClient]; emit('filter-change', rows.value);
} };
},
created () { const doFilter = () => {
this.addRow(); const clausoles = rows.value.filter(el => el.active).map(el => createClausole(el));
}, emit('filter', clausoles);
methods: { };
addRow () {
this.rows.push({ active: true, field: this.fields[0].name, op: '=', value: '', value2: '' }); const createClausole = (filter: TableFilterClausole) => {
this.$emit('filter-change', this.rows); const field = props.fields.find(field => field.name === filter.field);
},
removeRow (i) {
this.rows = this.rows.filter((_, idx) => idx !== i);
this.$emit('filter-change', this.rows);
},
doFilter () {
const clausoles = this.rows.filter(el => el.active).map(el => this.createClausole(el));
this.$emit('filter', clausoles);
},
createClausole (filter) {
const field = this.fields.find(field => field.name === filter.field);
const isNumeric = [...NUMBER, ...FLOAT].includes(field.type); const isNumeric = [...NUMBER, ...FLOAT].includes(field.type);
const { elementsWrapper: ew, stringsWrapper: sw } = this.customizations; const { elementsWrapper: ew, stringsWrapper: sw } = clientCustomizations.value;
let value; let value;
switch (filter.op) { switch (filter.op) {
@ -146,9 +140,9 @@ export default {
value = `${sw}${sw}`; value = `${sw}${sw}`;
return `${ew}${filter.field}${ew} ${filter.op} ${value}`; return `${ew}${filter.field}${ew} ${filter.op} ${value}`;
}
}
}; };
addRow();
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,12 +1,20 @@
// TODO: unfinished
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { ref } from 'vue'; import { Component, Ref, ref } from 'vue';
import { useNotificationsStore } from '@/stores/notifications';
import { TableDeleteParams, TableUpdateParams } from 'common/interfaces/tableApis';
const { addNotification } = useNotificationsStore();
export default function useResultTables (uid, reloadTable, addNotification) { export function useResultTables (uid: string, reloadTable: () => void) {
const tableRef = ref(null); const queryTable: Ref<Component & {
resetSort: () => void;
resizeResults: () => void;
refreshScroller: () => void;
downloadTable: (format: string, fileName: string) => void;
applyUpdate: (payload: TableUpdateParams) => void;
}> = ref(null);
const isQuering = ref(false); const isQuering = ref(false);
async function updateField (payload) { async function updateField (payload: TableUpdateParams) {
isQuering.value = true; isQuering.value = true;
const params = { const params = {
@ -20,7 +28,7 @@ export default function useResultTables (uid, reloadTable, addNotification) {
if (response.reload)// Needed for blob fields if (response.reload)// Needed for blob fields
reloadTable(); reloadTable();
else else
tableRef.value.applyUpdate(payload); queryTable.value.applyUpdate(payload);
} }
else else
addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
@ -32,7 +40,7 @@ export default function useResultTables (uid, reloadTable, addNotification) {
isQuering.value = false; isQuering.value = false;
} }
async function deleteSelected (payload) { async function deleteSelected (payload: TableDeleteParams) {
isQuering.value = true; isQuering.value = true;
const params = { const params = {
@ -56,7 +64,7 @@ export default function useResultTables (uid, reloadTable, addNotification) {
} }
return { return {
tableRef, queryTable,
isQuering, isQuering,
updateField, updateField,
deleteSelected deleteSelected

View File

@ -15,7 +15,7 @@ export default class {
return ipcRenderer.invoke('alter-function', unproxify(params)); return ipcRenderer.invoke('alter-function', unproxify(params));
} }
static alterTriggerFunction (params: {func: CreateFunctionParams & { uid: string }}): Promise<IpcResponse> { static alterTriggerFunction (params: { uid: string; func: AlterFunctionParams }): Promise<IpcResponse> {
return ipcRenderer.invoke('alter-trigger-function', unproxify(params)); return ipcRenderer.invoke('alter-trigger-function', unproxify(params));
} }

View File

@ -11,7 +11,7 @@ export default class {
return ipcRenderer.invoke('drop-routine', unproxify(params)); return ipcRenderer.invoke('drop-routine', unproxify(params));
} }
static alterRoutine (params: { routine: AlterRoutineParams & { uid: string } }): Promise<IpcResponse> { static alterRoutine (params: { uid: string; routine: AlterRoutineParams }): Promise<IpcResponse> {
return ipcRenderer.invoke('alter-routine', unproxify(params)); return ipcRenderer.invoke('alter-routine', unproxify(params));
} }

View File

@ -1,9 +1,9 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { unproxify } from '../libs/unproxify'; import { unproxify } from '../libs/unproxify';
import { AlterEventParams, CreateEventParams, EventInfos, IpcResponse } from 'common/interfaces/antares'; import { AlterEventParams, CreateEventParams, IpcResponse } from 'common/interfaces/antares';
export default class { export default class {
static getSchedulerInformations (params: { uid: string; schema: string; scheduler: string}): Promise<IpcResponse<EventInfos>> { static getSchedulerInformations (params: { uid: string; schema: string; scheduler: string}): Promise<IpcResponse> {
return ipcRenderer.invoke('get-scheduler-informations', unproxify(params)); return ipcRenderer.invoke('get-scheduler-informations', unproxify(params));
} }
@ -11,7 +11,7 @@ export default class {
return ipcRenderer.invoke('drop-scheduler', unproxify(params)); return ipcRenderer.invoke('drop-scheduler', unproxify(params));
} }
static alterScheduler (params: { scheduler: AlterEventParams & { uid: string } }): Promise<IpcResponse> { static alterScheduler (params: { uid: string; scheduler: AlterEventParams }): Promise<IpcResponse> {
return ipcRenderer.invoke('alter-scheduler', unproxify(params)); return ipcRenderer.invoke('alter-scheduler', unproxify(params));
} }

View File

@ -82,7 +82,7 @@ export default class {
return ipcRenderer.invoke('get-processes', uid); return ipcRenderer.invoke('get-processes', uid);
} }
static killProcess (params: { uid: string; pid: string }): Promise<IpcResponse> { static killProcess (params: { uid: string; pid: number }): Promise<IpcResponse> {
return ipcRenderer.invoke('kill-process', unproxify(params)); return ipcRenderer.invoke('kill-process', unproxify(params));
} }

View File

@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { unproxify } from '../libs/unproxify'; import { unproxify } from '../libs/unproxify';
import { AlterTableParams, CreateTableParams, IpcResponse, TableForeign, TableIndex, TableInfos } from 'common/interfaces/antares'; import { AlterTableParams, CreateTableParams, IpcResponse } from 'common/interfaces/antares';
export default class { export default class {
static getTableColumns (params: {schema: string; table: string }): Promise<IpcResponse> { static getTableColumns (params: {schema: string; table: string }): Promise<IpcResponse> {
@ -27,15 +27,15 @@ export default class {
return ipcRenderer.invoke('get-table-count', unproxify(params)); return ipcRenderer.invoke('get-table-count', unproxify(params));
} }
static getTableOptions (params: { uid: string; schema: string; table: string }): Promise<IpcResponse<TableInfos>> { static getTableOptions (params: { uid: string; schema: string; table: string }): Promise<IpcResponse> {
return ipcRenderer.invoke('get-table-options', unproxify(params)); return ipcRenderer.invoke('get-table-options', unproxify(params));
} }
static getTableIndexes (params: { uid: string; schema: string; table: string }): Promise<IpcResponse<TableIndex[]>> { static getTableIndexes (params: { uid: string; schema: string; table: string }): Promise<IpcResponse> {
return ipcRenderer.invoke('get-table-indexes', unproxify(params)); return ipcRenderer.invoke('get-table-indexes', unproxify(params));
} }
static getKeyUsage (params: { uid: string; schema: string; table: string }): Promise<IpcResponse<TableForeign[]>> { static getKeyUsage (params: { uid: string; schema: string; table: string }): Promise<IpcResponse> {
return ipcRenderer.invoke('get-key-usage', unproxify(params)); return ipcRenderer.invoke('get-key-usage', unproxify(params));
} }

View File

@ -1,53 +0,0 @@
import Tables from '@/ipc-api/Tables';
export default {
methods: {
async updateField (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
...payload
};
try {
const { status, response } = await Tables.updateTableCell(params);
if (status === 'success') {
if (response.reload)// Needed for blob fields
this.reloadTable();
else
this.$refs.queryTable.applyUpdate(payload);
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
async deleteSelected (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
...payload
};
try {
const { status, response } = await Tables.deleteTableRows(params);
this.isQuering = false;
if (status === 'success')
this.reloadTable();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
this.isQuering = false;
}
}
}
};