antares/src/renderer/components/WorkspaceExploreBar.vue

522 lines
15 KiB
Vue

<template>
<div class="column col-auto p-relative">
<div ref="resizer" class="workspace-explorebar-resizer" />
<div
ref="explorebar"
class="workspace-explorebar column"
:style="{width: localWidth ? localWidth+'px' : ''}"
tabindex="0"
@keypress="explorebarSearch"
@keydown="explorebarSearch"
>
<div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i
v-if="customizations.schemas"
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@click="showNewDBModal"
/>
<i
class="mdi mdi-18px mdi-refresh c-hand mr-2"
:class="{'rotate':isRefreshing}"
:title="$t('word.refresh')"
@click="refresh"
/>
<i
class="mdi mdi-18px mdi-power c-hand"
:title="$t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)"
/>
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connectionStatus === 'connected'" class="has-icon-right">
<input
ref="searchInput"
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-1"
@click="searchTerm = ''"
/>
</div>
</div>
<div class="workspace-explorebar-body" @click="explorebar.focus()">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
ref="schema"
:database="db"
:connection="connection"
@show-schema-context="openSchemaContext"
@show-table-context="openTableContext"
@show-misc-context="openMiscContext"
@show-misc-folder-context="openMiscFolderContext"
/>
</div>
</div>
<ModalNewSchema
v-if="isNewDBModal"
@close="hideNewDBModal"
@reload="refresh"
/>
<DatabaseContext
v-if="isDatabaseContext"
:selected-schema="selectedSchema"
:context-event="databaseContextEvent"
@close-context="closeDatabaseContext"
@open-create-table-tab="openCreateElementTab('table')"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@reload="refresh"
/>
<TableContext
v-if="isTableContext"
:selected-schema="selectedSchema"
:selected-table="selectedTable"
:context-event="tableContextEvent"
@delete-table="deleteTable"
@duplicate-table="duplicateTable"
@close-context="closeTableContext"
@reload="refresh"
/>
<MiscContext
v-if="isMiscContext"
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@close-context="closeMiscContext"
@reload="refresh"
/>
<MiscFolderContext
v-if="isMiscFolderContext"
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@close-context="closeMiscFolderContext"
@reload="refresh"
/>
</div>
</template>
<script setup lang="ts">
import { Component, computed, onMounted, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema.vue';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext.vue';
import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema.vue';
const props = defineProps({
connection: Object,
isSelected: Boolean
});
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { explorebarSize } = storeToRefs(settingsStore);
const { changeExplorebarSize } = settingsStore;
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
const searchInput: Ref<HTMLInputElement> = ref(null);
const explorebar: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: any }[]> = ref(null);
const isRefreshing = ref(false);
const isNewDBModal = ref(false);
const localWidth = ref(null);
const explorebarWidthInterval = ref(null);
const searchTermInterval = ref(null);
const isDatabaseContext = ref(false);
const isTableContext = ref(false);
const isMiscContext = ref(false);
const isMiscFolderContext = ref(false);
const databaseContextEvent = ref(null);
const tableContextEvent = ref(null);
const miscContextEvent = ref(null);
const selectedSchema = ref('');
const selectedTable = ref(null);
const selectedMisc = ref(null);
const searchTerm = ref('');
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const connectionName = computed(() => {
return getConnectionName(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
watch(localWidth, (val: number) => {
clearTimeout(explorebarWidthInterval.value);
explorebarWidthInterval.value = setTimeout(() => {
changeExplorebarSize(val);
}, 500);
});
watch(() => props.isSelected, (val: boolean) => {
if (val) localWidth.value = explorebarSize.value;
});
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
searchTermInterval.value = setTimeout(() => {
setSearchTerm(searchTerm.value);
}, 200);
});
localWidth.value = explorebarSize.value;
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
if (workspace.value.structure.length === 1) { // Auto-open if juust one schema
schema.value[0].selectSchema(workspace.value.structure[0].name);
schema.value[0].$refs.schemaAccordion.open = true;
}
});
const refresh = async () => {
if (!isRefreshing.value) {
isRefreshing.value = true;
await refreshStructure(props.connection.uid);
isRefreshing.value = false;
}
};
const explorebarSearch = (e: KeyboardEvent) => {
if (e.code === 'Backspace') {
e.preventDefault();
if (searchTerm.value.length)
searchTerm.value = searchTerm.value.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
searchInput.value.focus();
};
const resize = (e: MouseEvent) => {
const el = explorebar.value;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
if (explorebarWidth > 500) explorebarWidth = 500;
if (explorebarWidth < 150) explorebarWidth = 150;
localWidth.value = explorebarWidth;
};
const stopResize = () => {
window.removeEventListener('mousemove', resize);
};
const showNewDBModal = () => {
isNewDBModal.value = true;
};
const hideNewDBModal = () => {
isNewDBModal.value = false;
};
const openCreateElementTab = (element: string) => {
closeDatabaseContext();
closeMiscFolderContext();
newTab({
uid: workspace.value.uid,
schema: selectedSchema.value,
elementName: '',
elementType: element,
type: `new-${element}`
});
};
const openSchemaContext = (payload: { schema: string; event: PointerEvent }) => {
selectedSchema.value = payload.schema;
databaseContextEvent.value = payload.event;
isDatabaseContext.value = true;
};
const closeDatabaseContext = () => {
isDatabaseContext.value = false;
};
const openTableContext = (payload: { schema: string; table: string; event: PointerEvent }) => {
selectedTable.value = payload.table;
selectedSchema.value = payload.schema;
tableContextEvent.value = payload.event;
isTableContext.value = true;
};
const closeTableContext = () => {
isTableContext.value = false;
};
const openMiscContext = (payload: { schema: string; misc: string; event: PointerEvent }) => {
selectedMisc.value = payload.misc;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscContext.value = true;
};
const openMiscFolderContext = (payload: { schema: string; type: string; event: PointerEvent }) => {
selectedMisc.value = payload.type;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscFolderContext.value = true;
};
const closeMiscContext = () => {
isMiscContext.value = false;
};
const closeMiscFolderContext = () => {
isMiscFolderContext.value = false;
};
const deleteTable = async (payload: { schema: string; table: { name: string; type: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
let res;
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: props.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
refresh();
removeTabs({
uid: props.connection.uid as string,
elementName: payload.table.name as string,
elementType: payload.table.type,
schema: payload.schema as string
});
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
const duplicateTable = async (payload: { schema: string; table: { name: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
refresh();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
</script>
<style lang="scss">
.workspace-explorebar-resizer {
position: absolute;
width: 4px;
right: -2px;
top: 0;
height: calc(100vh - #{$excluding-size});
cursor: ew-resize;
z-index: 99;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 50%);
}
}
.workspace-explorebar {
width: $explorebar-width;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
text-align: left;
z-index: 8;
flex: initial;
position: relative;
padding: 0;
&:focus {
outline: none;
}
.workspace-explorebar-header {
width: 100%;
padding: 0.3rem;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
.workspace-explorebar-title {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
align-items: center;
}
.workspace-explorebar-tools {
display: flex;
align-items: center;
> i {
opacity: 0.6;
transition: opacity 0.2s;
display: flex;
align-items: center;
&:hover {
opacity: 1;
}
}
}
}
.workspace-explorebar-search {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
height: 28px;
.has-icon-right {
width: 100%;
padding: 0.1rem;
.form-icon {
opacity: 0.5;
transition: opacity 0.2s;
}
.form-input {
height: 1.2rem;
padding-left: 0.2rem;
&:focus + .form-icon {
opacity: 0.9;
}
&::placeholder {
opacity: 0.6;
}
}
}
}
.workspace-explorebar-body {
width: 100%;
height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay;
padding: 0 0.1rem;
}
}
</style>