apps skeleton

This commit is contained in:
Jan Prochazka 2022-01-27 14:31:46 +01:00
parent 8e3d614fb8
commit 595c9424df
16 changed files with 506 additions and 2 deletions

View File

@ -0,0 +1,100 @@
const fs = require('fs-extra');
const path = require('path');
const { appdir } = require('../utility/directories');
const socket = require('../utility/socket');
module.exports = {
folders_meta: true,
async folders() {
const folders = await fs.readdir(appdir());
return [
...folders.map(name => ({
name,
})),
];
},
createFolder_meta: true,
async createFolder({ folder }) {
await fs.mkdir(path.join(appdir(), folder));
socket.emitChanged('app-folders-changed');
return true;
},
files_meta: true,
async files({ folder }) {
const dir = path.join(appdir(), folder);
if (!(await fs.exists(dir))) return [];
const files = await fs.readdir(dir);
function fileType(ext, type) {
return files
.filter(name => name.endsWith(ext))
.map(name => ({
name: name.slice(0, -ext.length),
label: path.parse(name.slice(0, -ext.length)).base,
type,
}));
}
function refsType() {
return files
.filter(name => name == 'virtual-references.json')
.map(name => ({
name: 'virtual-references.json',
label: 'virtual-references.json',
type: 'vfk',
}));
}
return [...refsType(), ...fileType('.command.sql', 'command.sql'), ...fileType('.query.sql', 'query.sql')];
},
refreshFiles_meta: true,
async refreshFiles({ folder }) {
socket.emitChanged(`app-files-changed-${folder}`);
},
refreshFolders_meta: true,
async refreshFolders() {
socket.emitChanged(`app-folders-changed`);
},
deleteFile_meta: true,
async deleteFile({ folder, file, fileType }) {
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
socket.emitChanged(`app-files-changed-${folder}`);
},
renameFile_meta: true,
async renameFile({ folder, file, newFile, fileType }) {
await fs.rename(
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
);
socket.emitChanged(`app-files-changed-${folder}`);
},
renameFolder_meta: true,
async renameFolder({ folder, newFolder }) {
const uniqueName = await this.getNewAppFolder({ name: newFolder });
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
socket.emitChanged(`app-folders-changed`);
},
deleteFolder_meta: true,
async deleteFolder({ folder }) {
if (!folder) throw new Error('Missing folder parameter');
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
socket.emitChanged(`app-folders-changed`);
},
async getNewAppFolder({ name }) {
if (!(await fs.exists(path.join(appdir(), name)))) return name;
let index = 2;
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
index += 1;
}
return `${name}${index}`;
},
};

View File

@ -1,7 +1,7 @@
const uuidv1 = require('uuid/v1');
const fs = require('fs-extra');
const path = require('path');
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport');
const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket');
@ -74,6 +74,11 @@ module.exports = {
encoding: 'utf-8',
});
return deserialize(format, text);
} else if (folder.startsWith('app:')) {
const text = await fs.readFile(path.join(appdir(), folder.substring('app:'.length), file), {
encoding: 'utf-8',
});
return deserialize(format, text);
} else {
if (!hasPermission(`files/${folder}/read`)) return null;
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
@ -88,6 +93,10 @@ module.exports = {
await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
return true;
} else if (folder.startsWith('app:')) {
await fs.writeFile(path.join(appdir(), folder.substring('app:'.length), file), serialize(format, data));
socket.emitChanged(`app-files-changed-${folder.substring('app:'.length)}`);
return true;
} else {
if (!hasPermission(`files/${folder}/write`)) return false;
const dir = path.join(filesdir(), folder);

View File

@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
const jsldata = require('./controllers/jsldata');
const config = require('./controllers/config');
const archive = require('./controllers/archive');
const apps = require('./controllers/apps');
const uploads = require('./controllers/uploads');
const plugins = require('./controllers/plugins');
const files = require('./controllers/files');
@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
useController(app, electron, '/files', files);
useController(app, electron, '/scheduler', scheduler);
useController(app, electron, '/query-history', queryHistory);
useController(app, electron, '/apps', apps);
}
function initializeElectronSender(electronSender) {

View File

@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
const uploadsdir = dirFunc('uploads', true);
const pluginsdir = dirFunc('plugins');
const archivedir = dirFunc('archive');
const appdir = dirFunc('apps');
const filesdir = dirFunc('files');
function packagedPluginsDir() {
@ -103,6 +104,7 @@ module.exports = {
rundir,
uploadsdir,
archivedir,
appdir,
ensureDirectory,
pluginsdir,
filesdir,

View File

@ -0,0 +1,123 @@
<script lang="ts" context="module">
async function openTextFile(fileName, fileType, folderName, tabComponent, icon) {
const connProps: any = {};
let tooltip = undefined;
const resp = await apiCall('files/load', {
folder: 'app:' + folderName,
file: fileName + '.' + fileType,
format: 'text',
});
openNewTab(
{
title: fileName,
icon,
tabComponent,
tooltip,
props: {
savedFile: fileName + '.' + fileType,
savedFolder: 'app:' + folderName,
savedFormat: 'text',
appFolder: folderName,
...connProps,
},
},
{ editor: resp }
);
}
export const extractKey = data => data.fileName;
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
const APP_ICONS = {
'command.sql': 'img app-command',
'query.sql': 'img app-query',
};
function getAppIcon( data) {
return APP_ICONS[data.fileType];
}
</script>
<script lang="ts">
import _ from 'lodash';
import { filterName } from 'dbgate-tools';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import { archiveFilesAsDataSheets, currentArchive, extensions, getCurrentDatabase } from '../stores';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportElectronFile } from '../utility/exportElectronFile';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import InputTextModal from '../modals/InputTextModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import {
isArchiveFileMarkedAsDataSheet,
markArchiveFileAsDataSheet,
markArchiveFileAsReadonly,
} from '../utility/archiveTools';
import { apiCall } from '../utility/api';
export let data;
const handleRename = () => {
showModal(InputTextModal, {
value: data.fileName,
label: 'New file name',
header: 'Rename file',
onConfirm: newFile => {
apiCall('apps/rename-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
newFile,
});
},
});
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete file ${data.fileName}?`,
onConfirm: () => {
apiCall('apps/delete-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
});
},
});
};
const handleClick = () => {
if (data.fileType.endsWith('.sql')) {
handleOpenSqlFile();
}
};
const handleOpenSqlFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'QueryTab', 'img sql-file');
};
const handleOpenYamlFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'YamlEditorTab', 'img yaml');
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.fileLabel}
icon={getAppIcon( data)}
menu={createMenu}
on:click={handleClick}
/>

View File

@ -0,0 +1,64 @@
<script lang="ts" context="module">
export const extractKey = data => data.name;
export const createMatcher = data => filter => filterName(filter, data.name);
</script>
<script lang="ts">
import _ from 'lodash';
import { filterName } from 'dbgate-tools';
import { currentApplication } from '../stores';
import AppObjectCore from './AppObjectCore.svelte';
import { showModal } from '../modals/modalTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { apiCall } from '../utility/api';
export let data;
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete application ${data.name}?`,
onConfirm: () => {
apiCall('apps/delete-folder', { folder: data.name });
},
});
};
const handleRename = () => {
const { name } = data;
showModal(InputTextModal, {
value: name,
label: 'New application name',
header: 'Rename application',
onConfirm: async newFolder => {
await apiCall('apps/rename-folder', {
folder: data.name,
newFolder: newFolder,
});
if ($currentApplication == data.name) {
$currentApplication = newFolder;
}
},
});
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.name}
icon={'img app'}
isBold={data.name == $currentApplication}
on:click={() => ($currentApplication = data.name)}
menu={createMenu}
/>

View File

@ -81,7 +81,7 @@
markArchiveFileAsDataSheet,
markArchiveFileAsReadonly,
} from '../utility/archiveTools';
import { apiCall } from '../utility/api';
import { apiCall } from '../utility/api';
export let data;

View File

@ -128,6 +128,23 @@ registerCommand({
},
});
registerCommand({
id: 'new.application',
category: 'New',
icon: 'img app',
name: 'Application',
onClick: () => {
showModal(InputTextModal, {
value: '',
label: 'New application name',
header: 'Create application',
onConfirm: async folder => {
apiCall('apps/create-folder', { folder });
},
});
},
});
registerCommand({
id: 'new.table',
category: 'New',

View File

@ -26,6 +26,7 @@
'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
'icon app': 'mdi mdi-layers-triple',
'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline',
@ -126,6 +127,9 @@
'img diagram': 'mdi mdi-graph color-icon-blue',
'img yaml': 'mdi mdi-code-brackets color-icon-red',
'img compare': 'mdi mdi-compare color-icon-red',
'img app': 'mdi mdi-layers-triple color-icon-magenta',
'img app-command': 'mdi mdi-flash color-icon-green',
'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red',

View File

@ -64,6 +64,7 @@ export const openedModals = writable([]);
export const openedSnackbars = writable([]);
export const nullStore = readable(null, () => {});
export const currentArchive = writableWithStorage('default', 'currentArchive');
export const currentApplication = writableWithStorage(null, 'currentApplication');
export const isFileDragActive = writable(false);
export const selectedCellsCallback = writable(null);
export const loadingPluginStore = writable({

View File

@ -103,6 +103,18 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: `archive-files-changed-${folder}`,
});
const appFoldersLoader = () => ({
url: 'apps/folders',
params: {},
reloadTrigger: `app-folders-changed`,
});
const appFilesLoader = ({ folder }) => ({
url: 'apps/files',
params: { folder },
reloadTrigger: `app-files-changed-${folder}`,
});
const serverStatusLoader = () => ({
url: 'server-connections/server-status',
params: {},
@ -401,6 +413,20 @@ export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args);
}
export function getAppFiles(args) {
return getCore(appFilesLoader, args);
}
export function useAppFiles(args) {
return useCore(appFilesLoader, args);
}
export function getAppFolders(args = {}) {
return getCore(appFoldersLoader, args);
}
export function useAppFolders(args = {}) {
return useCore(appFoldersLoader, args);
}
export function getInstalledPlugins(args = {}) {
return getCore(installedPluginsLoader, args) || [];
}

View File

@ -0,0 +1,89 @@
<script lang="ts" context="module">
const APP_LABELS = {
'command.sql': 'SQL commands',
'query.sql': 'SQL queries',
};
</script>
<script lang="ts">
import { createFreeTableModel } from 'dbgate-datalib';
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFileAppObject from '../appobj/AppFileAppObject.svelte';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import DropDownButton from '../elements/DropDownButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import { currentApplication } from '../stores';
import { apiCall } from '../utility/api';
import { markArchiveFileAsDataSheet } from '../utility/archiveTools';
import { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folder = $currentApplication;
$: files = useAppFiles({ folder });
const handleRefreshFiles = () => {
apiCall('apps/refresh-files', { folder });
};
function handleNewSqlFile(fileType, header) {
showModal(InputTextModal, {
value: '',
label: 'New file name',
header,
onConfirm: async file => {
newQuery({
title: file,
// @ts-ignore
savedFile: file + '.' + fileType,
savedFolder: 'app:' + $currentApplication,
savedFormat: 'text',
appFolder: $currentApplication,
});
},
});
}
function createAddMenu() {
return [
{ text: 'New SQL command', onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command') },
{ text: 'New query view', onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query') },
];
}
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search application files" bind:value={filter} />
<CloseSearchButton bind:filter />
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
<InlineButton on:click={handleRefreshFiles} title="Refresh files of selected application">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList
list={($files || []).map(file => ({
fileName: file.name,
folderName: folder,
fileType: file.type,
fileLabel: file.label,
}))}
groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
module={appFileAppObject}
{filter}
/>
</WidgetsInnerContainer>

View File

@ -0,0 +1,39 @@
<script lang="ts">
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFolderAppObject from '../appobj/AppFolderAppObject.svelte';
import runCommand from '../commands/runCommand';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { apiCall } from '../utility/api';
import { useAppFolders } from '../utility/metadataLoaders';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folders = useAppFolders();
const handleRefreshFolders = () => {
apiCall('apps/refresh-folders');
};
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search applications" bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton on:click={() => runCommand('new.application')} title="Create new application">
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={handleRefreshFolders} title="Refresh application list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList list={_.sortBy($folders, 'name')} module={appFolderAppObject} {filter} />
</WidgetsInnerContainer>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import AppFilesList from './AppFilesList.svelte';
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import { useFavorites } from '../utility/metadataLoaders';
import AppFolderList from './AppFolderList.svelte';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Applications" name="apps" height="30%" storageName="appsWidget">
<AppFolderList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Application files" name="files" storageName="appFilesWidget">
<AppFilesList />
</WidgetColumnBarItem>
</WidgetColumnBar>

View File

@ -6,6 +6,7 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
</script>
<DatabaseWidget hidden={$selectedWidget != 'database'} />
@ -25,3 +26,6 @@
{#if $selectedWidget == 'cell-data'}
<CellDataWidget />
{/if}
{#if $selectedWidget == 'app'}
<AppWidget />
{/if}

View File

@ -40,6 +40,11 @@
name: 'cell-data',
title: 'Selected cell data detail view',
},
{
icon: 'icon app',
name: 'app',
title: 'Application layers',
},
// {
// icon: 'icon settings',
// name: 'settings',