Files
dbgate/packages/web/src/widgets/ConnectionList.svelte
2024-12-17 12:26:50 +01:00

363 lines
13 KiB
Svelte

<script lang="ts">
import _ from 'lodash';
import InlineButton from '../buttons/InlineButton.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { useConfig, useConnectionList, useServerStatus } from '../utility/metadataLoaders';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as connectionAppObject from '../appobj/ConnectionAppObject.svelte';
import SubDatabaseList from '../appobj/SubDatabaseList.svelte';
import {
commandsCustomized,
expandedConnections,
openedConnections,
openedSingleDatabaseConnections,
openedTabs,
emptyConnectionGroupNames,
collapsedConnectionGroupNames,
focusedConnectionOrDatabase,
getFocusedConnectionOrDatabase,
currentDatabase,
getCurrentConfig,
connectionAppObjectSearchSettings,
getConnectionAppObjectSearchSettings,
} from '../stores';
import runCommand from '../commands/runCommand';
import { filterName, getConnectionLabel } from 'dbgate-tools';
import { useConnectionColorFactory } from '../utility/useConnectionColor';
import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { apiCall, volatileConnectionMapStore } from '../utility/api';
import LargeButton from '../buttons/LargeButton.svelte';
import { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons';
import { safeJsonParse } from 'dbgate-tools';
import { showModal } from '../modals/modalTools';
import InputTextModal from '../modals/InputTextModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import AppObjectListHandler from './AppObjectListHandler.svelte';
import { getLocalStorage } from '../utility/storageCache';
import { switchCurrentDatabase } from '../utility/common';
import openNewTab from '../utility/openNewTab';
import { openConnection } from '../appobj/ConnectionAppObject.svelte';
import { getConnectionClickActionSetting } from '../settings/settingsTools';
import DropDownButton from '../buttons/DropDownButton.svelte';
const connections = useConnectionList();
const serverStatus = useServerStatus();
export let passProps: any = {};
let filter = '';
let domListHandler;
let domContainer = null;
let domFilter = null;
const RECENT_AND_UNSAVED_LABEL = 'Recent & unsaved';
function extractConnectionParent(data, openedConnections, openedSingleDatabaseConnections) {
if (data.parent) {
return data.parent;
}
if (data.unsaved && !openedConnections.includes(data._id) && !openedSingleDatabaseConnections.includes(data._id)) {
return RECENT_AND_UNSAVED_LABEL;
}
return null;
}
$: connectionsWithStatus =
$connections && $serverStatus
? $connections.map(conn => ({
...conn,
status: $serverStatus[$volatileConnectionMapStore[conn._id] || conn._id],
}))
: $connections;
$: connectionsWithParent = _.sortBy(
connectionsWithStatus
? connectionsWithStatus?.filter(x =>
extractConnectionParent(x, $openedConnections, $openedSingleDatabaseConnections)
)
: [],
connection => (getConnectionLabel(connection) || '').toUpperCase()
);
$: connectionsWithoutParent = _.sortBy(
connectionsWithStatus
? connectionsWithStatus?.filter(
x => !extractConnectionParent(x, $openedConnections, $openedSingleDatabaseConnections)
)
: [],
connection => (getConnectionLabel(connection) || '').toUpperCase()
);
function getFocusFlatList() {
const expanded = $expandedConnections;
const opened = $openedConnections;
const status = $serverStatus;
const res = [];
for (const con of [...connectionsWithParent, ...connectionsWithoutParent]) {
const databases = getLocalStorage(`database_list_${con._id}`) || [];
if (!filterName(filter, con.displayName, con.server, ...databases.map(x => x.name))) {
continue;
}
res.push({
connection: con,
conid: con._id,
database: con.singleDatabase ? con.defaultDatabase : null,
});
if ((expanded.includes(con._id) && opened.includes(con._id) && status?.[con._id]?.name == 'ok') || filter) {
for (const db of _.sortBy(databases, x => x.sortOrder ?? x.name)) {
if (!filterName(filter, con.displayName, con.server, db.name)) {
continue;
}
res.push({
conid: con._id,
database: db.name,
connection: con,
});
}
}
}
return res;
}
const handleRefreshConnections = () => {
for (const conid of $openedConnections) {
apiCall('server-connections/refresh', { conid });
}
};
const handleDropOnGroup = (data, group) => {
if (group == RECENT_AND_UNSAVED_LABEL) {
return;
}
const json = safeJsonParse(data);
if (json?._id) {
// if (json.parent) {
// emptyConnectionGroupNames.update(x => x.filter(y => y != json.parent));
// }
apiCall('connections/update', {
_id: json?._id,
values: { parent: group, unsaved: false },
});
}
};
const connectionColorFactory = useConnectionColorFactory(3);
function createGroupContextMenu(folder) {
const handleRename = () => {
showModal(InputTextModal, {
value: folder,
label: 'New folder name',
header: 'Rename folder',
onConfirm: async newFolder => {
emptyConnectionGroupNames.update(folders => _.uniq(folders.map(fld => (fld == folder ? newFolder : fld))));
apiCall('connections/batch-change-folder', {
folder,
newFolder,
});
},
});
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete folder ${folder}? Connections in folder will be moved into root folder.`,
onConfirm: () => {
emptyConnectionGroupNames.update(folders => folders.filter(fld => fld != folder));
apiCall('connections/batch-change-folder', {
folder,
newFolder: '',
});
},
});
};
return [
{ text: 'Rename', onClick: handleRename },
{ text: 'Delete', onClick: handleDelete },
];
}
function createSearchMenu() {
const res = [];
res.push({ label: 'Display name', switchValue: 'displayName' });
res.push({ label: 'Server', switchValue: 'server' });
res.push({ label: 'User', switchValue: 'user' });
res.push({ label: 'Database engine', switchValue: 'engine' });
res.push({ label: 'Database name', switchValue: 'database' });
return res.map(item => ({
...item,
switchStore: connectionAppObjectSearchSettings,
switchStoreGetter: getConnectionAppObjectSearchSettings,
}));
}
</script>
<SearchBoxWrapper>
<SearchInput
placeholder="Search connection or database"
bind:value={filter}
bind:this={domFilter}
onFocusFilteredList={() => {
domListHandler?.focusFirst();
}}
/>
<CloseSearchButton bind:filter />
<DropDownButton icon="icon filter" menu={createSearchMenu} square={!!filter} narrow={false} />
{#if $commandsCustomized['new.connection']?.enabled}
<InlineButton
on:click={() => runCommand('new.connection')}
title="Add new connection"
data-testid="ConnectionList_buttonNewConnection"
>
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={() => runCommand('new.connection.folder')} title="Add new connection folder">
<FontIcon icon="icon add-folder" />
</InlineButton>
{/if}
<InlineButton on:click={handleRefreshConnections} title="Refresh connection list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer
bind:this={domContainer}
on:drop={e => {
var data = e.dataTransfer.getData('app_object_drag_data');
if (data) {
handleDropOnGroup(data, '');
}
}}
>
<AppObjectListHandler
bind:this={domListHandler}
list={getFocusFlatList}
selectedObjectStore={focusedConnectionOrDatabase}
getSelectedObject={getFocusedConnectionOrDatabase}
selectedObjectMatcher={(o1, o2) => o1?.conid == o2?.conid && o1?.database == o2?.database}
getDefaultFocusedItem={() =>
$currentDatabase
? {
conid: $currentDatabase?.connection?._id,
database: $currentDatabase?.name,
connection: $currentDatabase?.connection,
}
: null}
onScrollTop={() => {
domContainer?.scrollTop();
}}
onFocusFilterBox={text => {
domFilter?.focus(text);
}}
handleObjectClick={(data, options) => {
const connectionClickAction = getConnectionClickActionSetting();
if (data.database) {
if (options.focusTab) {
if ($openedSingleDatabaseConnections.includes(data.conid)) {
switchCurrentDatabase({ connection: data.connection, name: data.database });
} else {
switchCurrentDatabase({ connection: data.connection, name: data.database });
}
// console.log('FOCUSING DB', passProps);
// passProps?.onFocusSqlObjectList?.();
}
} else {
if (options.focusTab) {
openConnection(data.connection);
} else {
const config = getCurrentConfig();
if (config.runAsPortal == false && !config.storageDatabase && connectionClickAction == 'openDetails') {
openNewTab({
title: getConnectionLabel(data.connection),
icon: 'img connection',
tabComponent: 'ConnectionTab',
tabPreviewMode: options.tabPreviewMode,
props: {
conid: data.conid,
},
});
}
}
}
}}
handleExpansion={(item, value) => {
if (item.database) {
return;
}
expandedConnections.update(old => (value ? [...old, item.conid] : old.filter(x => x != item.conid)));
}}
>
<AppObjectList
list={connectionsWithParent}
module={connectionAppObject}
subItemsComponent={() => SubDatabaseList}
expandOnClick
isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase}
{filter}
passProps={{
...passProps,
connectionColorFactory: $connectionColorFactory,
showPinnedInsteadOfUnpin: true,
searchSettings: $connectionAppObjectSearchSettings,
}}
getIsExpanded={data => $expandedConnections.includes(data._id) && !data.singleDatabase}
setIsExpanded={(data, value) => {
expandedConnections.update(old => (value ? [...old, data._id] : old.filter(x => x != data._id)));
}}
groupIconFunc={chevronExpandIcon}
groupFunc={data => extractConnectionParent(data, $openedConnections, $openedSingleDatabaseConnections)}
expandIconFunc={plusExpandIcon}
onDropOnGroup={handleDropOnGroup}
emptyGroupNames={$emptyConnectionGroupNames}
sortGroups
groupContextMenu={createGroupContextMenu}
collapsedGroupNames={collapsedConnectionGroupNames}
/>
{#if (connectionsWithParent?.length > 0 && connectionsWithoutParent?.length > 0) || ($emptyConnectionGroupNames.length > 0 && connectionsWithoutParent?.length > 0)}
<div class="br" />
{/if}
<AppObjectList
list={connectionsWithoutParent}
module={connectionAppObject}
subItemsComponent={() => SubDatabaseList}
expandOnClick
isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase}
{filter}
passProps={{
connectionColorFactory: $connectionColorFactory,
showPinnedInsteadOfUnpin: true,
searchSettings: $connectionAppObjectSearchSettings,
}}
getIsExpanded={data => $expandedConnections.includes(data._id) && !data.singleDatabase}
setIsExpanded={(data, value) => {
expandedConnections.update(old => (value ? [...old, data._id] : old.filter(x => x != data._id)));
}}
/>
</AppObjectListHandler>
{#if $connections && $connections.length == 0 && $openedConnections.length == 0 && $commandsCustomized['new.connection']?.enabled && !$openedTabs.find(x => !x.closedTime && x.tabComponent == 'ConnectionTab' && !x.props?.conid)}
<LargeButton icon="icon new-connection" on:click={() => runCommand('new.connection')} fillHorizontal
>Add new connection</LargeButton
>
<!-- <ToolbarButton icon="icon new-connection" on:click={() => runCommand('new.connection')}>
Add new connection
</ToolbarButton> -->
{/if}
</WidgetsInnerContainer>
<style>
.br {
background: var(--theme-bg-2);
height: 1px;
margin: 5px 10px;
}
</style>