mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-26 16:46:00 +00:00
shell tab
This commit is contained in:
@@ -5,6 +5,7 @@ import { ThemeDefinition } from 'dbgate-types';
|
||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import saveTabFile, { saveTabEnabledStore } from '../utility/saveTabFile';
|
||||
|
||||
function themeCommand(theme: ThemeDefinition) {
|
||||
return {
|
||||
@@ -64,3 +65,94 @@ registerCommand({
|
||||
keyText: 'Ctrl+Q',
|
||||
onClick: () => newQuery(),
|
||||
});
|
||||
|
||||
export function registerFileCommands({
|
||||
idPrefix,
|
||||
category,
|
||||
editorStore,
|
||||
editorStatusStore,
|
||||
folder,
|
||||
format,
|
||||
fileExtension,
|
||||
execute = false,
|
||||
toggleComment = false,
|
||||
findReplace = false,
|
||||
}) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.save',
|
||||
category,
|
||||
name: 'Save',
|
||||
keyText: 'Ctrl+S',
|
||||
icon: 'icon save',
|
||||
toolbar: true,
|
||||
enabledStore: saveTabEnabledStore(editorStore),
|
||||
onClick: () => saveTabFile(editorStore, false, folder, format, fileExtension),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.saveAs',
|
||||
category,
|
||||
name: 'Save As',
|
||||
keyText: 'Ctrl+Shift+S',
|
||||
enabledStore: saveTabEnabledStore(editorStore),
|
||||
onClick: () => saveTabFile(editorStore, true, folder, format, fileExtension),
|
||||
});
|
||||
|
||||
if (execute) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.execute',
|
||||
category,
|
||||
name: 'Execute',
|
||||
icon: 'icon run',
|
||||
toolbar: true,
|
||||
keyText: 'F5 | Ctrl+Enter',
|
||||
enabledStore: derived(
|
||||
[editorStore, editorStatusStore],
|
||||
([editor, status]) => editor != null && !(status as any).busy
|
||||
),
|
||||
onClick: () => (get(editorStore) as any).execute(),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.kill',
|
||||
category,
|
||||
name: 'Kill',
|
||||
icon: 'icon close',
|
||||
toolbar: true,
|
||||
enabledStore: derived(
|
||||
[editorStore, editorStatusStore],
|
||||
([query, status]) => query != null && status && (status as any).isConnected
|
||||
),
|
||||
onClick: () => (get(editorStore) as any).kill(),
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleComment) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.toggleComment',
|
||||
category,
|
||||
name: 'Toggle comment',
|
||||
keyText: 'Ctrl+/',
|
||||
disableHandleKeyText: 'Ctrl+/',
|
||||
enabledStore: derived(editorStore, query => query != null),
|
||||
onClick: () => (get(editorStore) as any).toggleComment(),
|
||||
});
|
||||
}
|
||||
|
||||
if (findReplace) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.find',
|
||||
category,
|
||||
name: 'Find',
|
||||
keyText: 'Ctrl+F',
|
||||
enabledStore: derived(editorStore, query => query != null),
|
||||
onClick: () => (get(editorStore) as any).find(),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.replace',
|
||||
category,
|
||||
keyText: 'Ctrl+H',
|
||||
name: 'Replace',
|
||||
enabledStore: derived(editorStore, query => query != null),
|
||||
onClick: () => (get(editorStore) as any).replace(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
86
packages/web/src/elements/TableControl.svelte
Normal file
86
packages/web/src/elements/TableControl.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
import { compact } from 'lodash';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface TableColumn {
|
||||
fieldName: string;
|
||||
header: string;
|
||||
component?: any;
|
||||
getProps?: any;
|
||||
formatter?: any;
|
||||
}
|
||||
|
||||
export let columns: TableColumn[];
|
||||
export let rows;
|
||||
export let focusOnCreate = false;
|
||||
export let selectable = false;
|
||||
export let selectedIndex = 0;
|
||||
|
||||
$: columnList = _.compact(_.flatten(columns));
|
||||
|
||||
let domTable;
|
||||
|
||||
onMount(() => {
|
||||
if (focusOnCreate) domTable.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<table bind:this={domTable} class:selectable>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columnList as col}
|
||||
<td>{col.header}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row, index}
|
||||
<tr
|
||||
class:selected={selectable && selectedIndex == index}
|
||||
on:click={() => {
|
||||
if (selectable) {
|
||||
selectedIndex = index;
|
||||
domTable.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each columnList as col}
|
||||
{#if col.component}
|
||||
<svelte:component this={col.component} {...col.getProps(row)} />
|
||||
{:else if col.formatter}
|
||||
{col.formatter(row)}
|
||||
{:else}
|
||||
{row[col.fieldName]}
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
table.selectable {
|
||||
user-select: none;
|
||||
}
|
||||
tbody tr {
|
||||
background: var(--theme-bg-0);
|
||||
}
|
||||
tbody tr.selected {
|
||||
background: var(--theme-bg-selected);
|
||||
}
|
||||
thead td {
|
||||
border: 1px solid var(--theme-border);
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
tbody td {
|
||||
border: 1px solid var(--theme-border);
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
19
packages/web/src/query/RunnerOutputFiles/CopyLink.svelte
Normal file
19
packages/web/src/query/RunnerOutputFiles/CopyLink.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import getElectron from '../../utility/getElectron';
|
||||
|
||||
const electron = getElectron();
|
||||
export let row;
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
on:click={() => {
|
||||
const file = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), {});
|
||||
if (file) {
|
||||
const fs = window.require('fs');
|
||||
fs.copyFile(row.path, file, () => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
save
|
||||
</a>
|
||||
10
packages/web/src/query/RunnerOutputFiles/DownloadLink.svelte
Normal file
10
packages/web/src/query/RunnerOutputFiles/DownloadLink.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import resolveApi from '../../utility/resolveApi';
|
||||
|
||||
export let runnerId;
|
||||
export let row;
|
||||
</script>
|
||||
|
||||
<a href={`${resolveApi()}/runners/data/${runnerId}/${row.name}`} target="_blank" rel="noopener noreferrer">
|
||||
download
|
||||
</a>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import ErrorInfo from '../../elements/ErrorInfo.svelte';
|
||||
import TableControl from '../../elements/TableControl.svelte';
|
||||
|
||||
import axiosInstance from '../../utility/axiosInstance';
|
||||
import formatFileSize from '../../utility/formatFileSize';
|
||||
import getElectron from '../../utility/getElectron';
|
||||
import socket from '../../utility/socket';
|
||||
import useEffect from '../../utility/useEffect';
|
||||
import CopyLink from './CopyLink.svelte';
|
||||
import DownloadLink from './DownloadLink.svelte';
|
||||
import ShowLink from './ShowLink.svelte';
|
||||
|
||||
export let runnerId;
|
||||
export let executeNumber;
|
||||
|
||||
let files = [];
|
||||
|
||||
$: if (executeNumber >= 0) files = [];
|
||||
|
||||
$: effect = useEffect(() => registerRunnerDone(runnerId));
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
function registerRunnerDone(rid) {
|
||||
if (rid) {
|
||||
socket.on(`runner-done-${rid}`, handleRunnerDone);
|
||||
return () => {
|
||||
socket.off(`runner-done-${rid}`, handleRunnerDone);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
$: $effect;
|
||||
|
||||
const handleRunnerDone = async () => {
|
||||
const resp = await axiosInstance.get(`runners/files?runid=${runnerId}`);
|
||||
files = resp.data;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !files || files.length == 0}
|
||||
<ErrorInfo message="No output files" icon="img alert" />
|
||||
{/if}
|
||||
|
||||
<TableControl
|
||||
rows={files}
|
||||
columns={[
|
||||
{ fieldName: 'name', header: 'Name' },
|
||||
{ fieldName: 'size', header: 'Size', formatter: row => formatFileSize(row.size) },
|
||||
!electron && {
|
||||
fieldName: 'download',
|
||||
header: 'Download',
|
||||
component: DownloadLink,
|
||||
getProps: row => ({
|
||||
row,
|
||||
runnerId,
|
||||
}),
|
||||
},
|
||||
!electron && {
|
||||
fieldName: 'copy',
|
||||
header: 'Copy',
|
||||
component: CopyLink,
|
||||
getProps: row => ({
|
||||
row,
|
||||
runnerId,
|
||||
}),
|
||||
},
|
||||
!electron && {
|
||||
fieldName: 'show',
|
||||
header: 'Show',
|
||||
component: ShowLink,
|
||||
getProps: row => ({
|
||||
row,
|
||||
runnerId,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
15
packages/web/src/query/RunnerOutputFiles/ShowLink.svelte
Normal file
15
packages/web/src/query/RunnerOutputFiles/ShowLink.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import getElectron from '../../utility/getElectron';
|
||||
|
||||
const electron = getElectron();
|
||||
export let row;
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
on:click={() => {
|
||||
electron.remote.shell.showItemInFolder(row.path);
|
||||
}}
|
||||
>
|
||||
show
|
||||
</a>
|
||||
1
packages/web/src/query/RunnerOutputFiles/index.js
Normal file
1
packages/web/src/query/RunnerOutputFiles/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './RunnerOutputFiles.svelte';
|
||||
28
packages/web/src/query/RunnerOutputPane.svelte
Normal file
28
packages/web/src/query/RunnerOutputPane.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||
import WidgetTitle from '../widgets/WidgetTitle.svelte';
|
||||
import RunnerOutputFiles from './RunnerOutputFiles';
|
||||
import SocketMessageView from './SocketMessageView.svelte';
|
||||
|
||||
export let runnerId;
|
||||
export let executeNumber;
|
||||
</script>
|
||||
|
||||
<HorizontalSplitter>
|
||||
<div class="container">
|
||||
<WidgetTitle>Messages</WidgetTitle>
|
||||
<SocketMessageView eventName={runnerId ? `runner-info-${runnerId}` : null} {executeNumber} />
|
||||
</div>
|
||||
<div class="container">
|
||||
<WidgetTitle>Output files</WidgetTitle>
|
||||
<RunnerOutputFiles {runnerId} {executeNumber} />
|
||||
</div>
|
||||
</HorizontalSplitter>
|
||||
|
||||
<style>
|
||||
div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -5,40 +5,6 @@
|
||||
);
|
||||
const currentQueryStatus = memberStore(currentQuery, query => query?.getStatus() || nullStore);
|
||||
|
||||
registerCommand({
|
||||
id: 'query.execute',
|
||||
category: 'Query',
|
||||
name: 'Execute',
|
||||
icon: 'icon run',
|
||||
toolbar: true,
|
||||
keyText: 'F5 | Ctrl+Enter',
|
||||
enabledStore: derived(
|
||||
[currentQuery, currentQueryStatus],
|
||||
([query, status]) => query != null && !(status as any).busy
|
||||
),
|
||||
onClick: () => get(currentQuery).execute(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'query.kill',
|
||||
category: 'Query',
|
||||
name: 'Kill',
|
||||
icon: 'icon close',
|
||||
toolbar: true,
|
||||
enabledStore: derived(
|
||||
[currentQuery, currentQueryStatus],
|
||||
([query, status]) => query != null && status && (status as any).isConnected
|
||||
),
|
||||
onClick: () => get(currentQuery).kill(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'query.toggleComment',
|
||||
category: 'Query',
|
||||
name: 'Toggle comment',
|
||||
keyText: 'Ctrl+/',
|
||||
disableHandleKeyText: 'Ctrl+/',
|
||||
enabledStore: derived(currentQuery, query => query != null),
|
||||
onClick: () => get(currentQuery).toggleComment(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'query.formatCode',
|
||||
category: 'Query',
|
||||
@@ -46,29 +12,18 @@
|
||||
enabledStore: derived(currentQuery, query => query != null),
|
||||
onClick: () => get(currentQuery).formatCode(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'query.find',
|
||||
category: 'Query',
|
||||
name: 'Find',
|
||||
keyText: 'Ctrl+F',
|
||||
enabledStore: derived(currentQuery, query => query != null),
|
||||
onClick: () => get(currentQuery).find(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'query.replace',
|
||||
category: 'Query',
|
||||
keyText: 'Ctrl+H',
|
||||
name: 'Replace',
|
||||
enabledStore: derived(currentQuery, query => query != null),
|
||||
onClick: () => get(currentQuery).replace(),
|
||||
});
|
||||
registerSaveCommands({
|
||||
registerFileCommands({
|
||||
idPrefix: 'query',
|
||||
category: 'Query',
|
||||
editorStore: currentQuery,
|
||||
editorStatusStore: currentQueryStatus,
|
||||
folder: 'sql',
|
||||
format: 'text',
|
||||
fileExtension: 'sql',
|
||||
|
||||
execute: true,
|
||||
toggleComment:true,
|
||||
findReplace:true
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -93,7 +48,7 @@
|
||||
import memberStore from '../utility/memberStore';
|
||||
import useEffect from '../utility/useEffect';
|
||||
import ResultTabs from '../query/ResultTabs.svelte';
|
||||
import saveTabFile, { registerSaveCommands, saveTabEnabledStore } from '../utility/saveTabFile';
|
||||
import { registerFileCommands } from '../commands/stdCommands';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
|
||||
6
packages/web/src/utility/formatFileSize.ts
Normal file
6
packages/web/src/utility/formatFileSize.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function formatFileSize(size) {
|
||||
if (size > 1000000000) return `${Math.round(size / 10000000000) * 10} GB`;
|
||||
if (size > 1000000) return `${Math.round(size / 10000000) * 10} MB`;
|
||||
if (size > 1000) return `${Math.round(size / 10000) * 10} KB`;
|
||||
return `${size} bytes`;
|
||||
}
|
||||
@@ -52,24 +52,3 @@ export default function saveTabFile(editorStore, saveAs, folder, format, fileExt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSaveCommands({ idPrefix, category, editorStore, folder, format, fileExtension }) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.save',
|
||||
category,
|
||||
name: 'Save',
|
||||
keyText: 'Ctrl+S',
|
||||
icon: 'icon save',
|
||||
toolbar: true,
|
||||
enabledStore: saveTabEnabledStore(editorStore),
|
||||
onClick: () => saveTabFile(editorStore, false, folder, format, fileExtension),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.saveAs',
|
||||
category,
|
||||
name: 'Save As',
|
||||
keyText: 'Ctrl+Shift+S',
|
||||
enabledStore: saveTabEnabledStore(editorStore),
|
||||
onClick: () => saveTabFile(editorStore, true, folder, format, fileExtension),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import WidgetTitle from './WidgetTitle.svelte';
|
||||
|
||||
export let title;
|
||||
export let name;
|
||||
export let height = null;
|
||||
@@ -7,21 +9,8 @@
|
||||
let visible = !collapsed;
|
||||
</script>
|
||||
|
||||
<div class="title" on:click={() => (visible = !visible)}>{title}</div>
|
||||
<WidgetTitle on:click={() => (visible = !visible)}>{title}</WidgetTitle>
|
||||
|
||||
{#if visible}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.title {
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
background-color: var(--theme-bg-2);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
.title:hover {
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
16
packages/web/src/widgets/WidgetTitle.svelte
Normal file
16
packages/web/src/widgets/WidgetTitle.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<div on:click>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
background-color: var(--theme-bg-2);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
div:hover {
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user