markdown manifest

This commit is contained in:
Jan Prochazka
2020-12-10 18:34:02 +01:00
parent d502dc0dfd
commit ac4aa94976
16 changed files with 187 additions and 74 deletions

View File

@@ -3,16 +3,16 @@ const currentVersion = require('../currentVersion');
module.exports = { module.exports = {
get_meta: 'get', get_meta: 'get',
async get() { async get() {
const toolbarButtons = process.env.TOOLBAR; // const toolbarButtons = process.env.TOOLBAR;
const toolbar = toolbarButtons // const toolbar = toolbarButtons
? toolbarButtons.split(',').map((name) => ({ // ? toolbarButtons.split(',').map((name) => ({
name, // name,
icon: process.env[`ICON_${name}`], // icon: process.env[`ICON_${name}`],
title: process.env[`TITLE_${name}`], // title: process.env[`TITLE_${name}`],
page: process.env[`PAGE_${name}`], // page: process.env[`PAGE_${name}`],
})) // }))
: null; // : null;
const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : []; // const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null; const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
const singleDatabase = const singleDatabase =
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
@@ -24,8 +24,8 @@ module.exports = {
return { return {
runAsPortal: !!process.env.CONNECTIONS, runAsPortal: !!process.env.CONNECTIONS,
toolbar, // toolbar,
startupPages, // startupPages,
singleDatabase, singleDatabase,
permissions, permissions,
...currentVersion, ...currentVersion,

View File

@@ -5,6 +5,10 @@ const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
const scheduler = require('./scheduler'); const scheduler = require('./scheduler');
const markdownAutorunRegex = /<\!--.*@autorun\s*(\n.*-->|-->)/s;
const markdownButtonRegex = /<\!--.*@button\s+([^\n]+)(\n.*-->|-->)/s;
const markdownIconRegex = /<\!--.*@icon\s+([^\n]+)(\n.*-->|-->)/s;
function serialize(format, data) { function serialize(format, data) {
if (format == 'text') return data; if (format == 'text') return data;
if (format == 'json') return JSON.stringify(data); if (format == 'json') return JSON.stringify(data);
@@ -61,4 +65,29 @@ module.exports = {
scheduler.reload(); scheduler.reload();
} }
}, },
markdownManifest_meta: 'get',
async markdownManifest() {
if (!hasPermission(`files/markdown/write`)) return {};
const dir = path.join(filesdir(), 'markdown');
if (!(await fs.exists(dir))) return {};
const files = await fs.readdir(dir);
const res = [];
for (const file of files) {
const filePath = path.join(dir, file);
const text = await fs.readFile(filePath, { encoding: 'utf-8' });
const autorun = text.match(markdownAutorunRegex);
const button = text.match(markdownButtonRegex);
const icon = text.match(markdownIconRegex);
if (autorun || button) {
res.push({
file,
autorun: !!autorun,
button: button ? button[1].trim() : undefined,
icon: icon ? icon[1].trim() : undefined,
});
}
}
return res;
},
}; };

View File

@@ -72,9 +72,9 @@ function start(argument = null) {
useController(app, '/files', files); useController(app, '/files', files);
useController(app, '/scheduler', scheduler); useController(app, '/scheduler', scheduler);
if (process.env.PAGES_DIRECTORY) { // if (process.env.PAGES_DIRECTORY) {
app.use('/pages', express.static(process.env.PAGES_DIRECTORY)); // app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
} // }
app.use('/runners/data', express.static(rundir())); app.use('/runners/data', express.static(rundir()));

View File

@@ -26,6 +26,7 @@ const iconNames = {
'icon account': 'mdi mdi-account', 'icon account': 'mdi mdi-account',
'icon sql-file': 'mdi mdi-file', 'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web', 'icon web': 'mdi mdi-web',
'icon home': 'mdi mdi-home',
'icon edit': 'mdi mdi-pencil', 'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete', 'icon delete': 'mdi mdi-delete',
@@ -40,6 +41,7 @@ const iconNames = {
'icon error': 'mdi mdi-close-circle', 'icon error': 'mdi mdi-close-circle',
'icon ok': 'mdi mdi-check-circle', 'icon ok': 'mdi mdi-check-circle',
'icon markdown': 'mdi mdi-application', 'icon markdown': 'mdi mdi-application',
'icon preview': 'mdi mdi-file-find',
'icon run': 'mdi mdi-play', 'icon run': 'mdi mdi-play',
'icon chevron-down': 'mdi mdi-chevron-down', 'icon chevron-down': 'mdi mdi-chevron-down',
@@ -62,6 +64,7 @@ const iconNames = {
'img shell': 'mdi mdi-flash color-blue-7', 'img shell': 'mdi mdi-flash color-blue-7',
'img chart': 'mdi mdi-chart-bar color-magenta-7', 'img chart': 'mdi mdi-chart-bar color-magenta-7',
'img markdown': 'mdi mdi-application color-red-7', 'img markdown': 'mdi mdi-application color-red-7',
'img preview': 'mdi mdi-file-find color-red-7',
'img free-table': 'mdi mdi-table color-green-7', 'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench', 'img macro': 'mdi mdi-hammer-wrench',

View File

@@ -2,6 +2,7 @@ import React from 'react';
import Markdown from 'markdown-to-jsx'; import Markdown from 'markdown-to-jsx';
import styled from 'styled-components'; import styled from 'styled-components';
import OpenChartLink from './OpenChartLink'; import OpenChartLink from './OpenChartLink';
import MarkdownLink from './MarkdownLink';
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 10px; padding: 10px;
@@ -18,6 +19,7 @@ export default function MarkdownExtendedView({ children }) {
OpenChartLink: { OpenChartLink: {
component: OpenChartLink, component: OpenChartLink,
}, },
a: MarkdownLink,
}, },
}} }}
> >

View File

@@ -0,0 +1,13 @@
import React from 'react';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
export default function MarkdownLink({ href, title, children }) {
const theme = useTheme();
return (
<StyledThemedLink theme={theme} href={href} title={title} target="_blank">
{children}
</StyledThemedLink>
);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import useHasPermission from '../utility/useHasPermission'; import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; import ToolbarButton from '../widgets/ToolbarButton';
export default function MarkdownToolbar({ save }) { export default function MarkdownToolbar({ save, showPreview }) {
const hasPermission = useHasPermission(); const hasPermission = useHasPermission();
return ( return (
@@ -12,6 +12,9 @@ export default function MarkdownToolbar({ save }) {
Save Save
</ToolbarButton> </ToolbarButton>
)} )}
<ToolbarButton onClick={showPreview} icon="icon preview">
Preview
</ToolbarButton>
</> </>
); );
} }

View File

@@ -1,18 +1,9 @@
import React from 'react'; import React from 'react';
import { useCurrentDatabase, useSetOpenedTabs } from '../utility/globalState'; import { useCurrentDatabase, useSetOpenedTabs } from '../utility/globalState';
import styled from 'styled-components';
import { openNewTab } from '../utility/common'; import { openNewTab } from '../utility/common';
import axios from '../utility/axios'; import axios from '../utility/axios';
import useTheme from '../theme/useTheme'; import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
const StyledLink = styled.a`
text-decoration: none;
cursor: pointer;
color: ${(props) => props.theme.main_background_blue[7]};
&:hover {
text-decoration: underline;
}
`;
export default function OpenChartLink({ file, children }) { export default function OpenChartLink({ file, children }) {
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
@@ -37,8 +28,8 @@ export default function OpenChartLink({ file, children }) {
}; };
return ( return (
<StyledLink theme={theme} onClick={handleClick}> <StyledThemedLink theme={theme} onClick={handleClick}>
{children} {children}
</StyledLink> </StyledThemedLink>
); );
} }

View File

@@ -9,6 +9,7 @@ import moment from 'moment';
import styled from 'styled-components'; import styled from 'styled-components';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
import useTheme from '../theme/useTheme'; import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
@@ -24,15 +25,6 @@ const StyledValue = styled.span`
font-weight: bold; font-weight: bold;
`; `;
const StyledLink = styled.a`
text-decoration: none;
cursor: pointer;
color: ${(props) => props.theme.main_background_blue[7]};
&:hover {
text-decoration: underline;
}
`;
function Line({ label, children }) { function Line({ label, children }) {
return ( return (
<StyledLine> <StyledLine>
@@ -48,13 +40,13 @@ function Link({ label, children, href }) {
<StyledLine> <StyledLine>
{label}:{' '} {label}:{' '}
{electron ? ( {electron ? (
<StyledLink theme={theme} onClick={() => electron.shell.openExternal(href)}> <StyledThemedLink theme={theme} onClick={() => electron.shell.openExternal(href)}>
{children} {children}
</StyledLink> </StyledThemedLink>
) : ( ) : (
<StyledLink theme={theme} href={href} target="_blank" rel="noopener noreferrer"> <StyledThemedLink theme={theme} href={href} target="_blank" rel="noopener noreferrer">
{children} {children}
</StyledLink> </StyledThemedLink>
)} )}
</StyledLine> </StyledLine>
); );

View File

@@ -8,15 +8,42 @@ import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal'; import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState'; import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo'; import LoadingInfo from '../widgets/LoadingInfo';
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
import { openNewTab } from '../utility/common';
export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) { export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
const { editorData, setEditorData, isLoading } = useEditorData({ tabid }); const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid });
const saveFileModalState = useModalState(); const saveFileModalState = useModalState();
const openedTabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
const handleKeyDown = (data, hash, keyString, keyCode, event) => { const handleKeyDown = (data, hash, keyString, keyCode, event) => {
if (keyCode == keycodes.f5) { if (keyCode == keycodes.f5) {
event.preventDefault(); event.preventDefault();
// handlePreview(); showPreview();
}
};
const showPreview = async () => {
await saveToStorage();
const existing = (openedTabs || []).find((x) => x.props && x.props.sourceTabId == tabid && x.closedTime == null);
if (existing) {
setOpenedTabs((tabs) =>
tabs.map((x) => ({
...x,
selected: x.tabid == existing.tabid,
}))
);
} else {
const thisTab = (openedTabs || []).find((x) => x.tabid == tabid);
openNewTab(setOpenedTabs, {
title: thisTab.title,
icon: 'img preview',
tabComponent: 'MarkdownPreviewTab',
props: {
sourceTabId: tabid,
},
});
} }
}; };
@@ -40,7 +67,10 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
{toolbarPortalRef && {toolbarPortalRef &&
toolbarPortalRef.current && toolbarPortalRef.current &&
tabVisible && tabVisible &&
ReactDOM.createPortal(<MarkdownToolbar save={saveFileModalState.open} />, toolbarPortalRef.current)} ReactDOM.createPortal(
<MarkdownToolbar save={saveFileModalState.open} showPreview={showPreview} />,
toolbarPortalRef.current
)}
<SaveTabModal <SaveTabModal
modalState={saveFileModalState} modalState={saveFileModalState}
tabVisible={tabVisible} tabVisible={tabVisible}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import LoadingInfo from '../widgets/LoadingInfo';
import MarkdownExtendedView from '../markdown/MarkdownExtendedView';
import useEditorData from '../utility/useEditorData';
export default function MarkdownPreviewTab({ sourceTabId, tabVisible }) {
const [reloadToken, setReloadToken] = React.useState(0);
const { editorData, isLoading } = useEditorData({ tabid: sourceTabId, reloadToken });
React.useEffect(() => {
if (tabVisible) setReloadToken((x) => x + 1);
}, [tabVisible]);
if (isLoading) {
return (
<div>
<LoadingInfo message="Loading markdown page" />
</div>
);
}
return <MarkdownExtendedView>{editorData || ''}</MarkdownExtendedView>;
}

View File

@@ -10,6 +10,7 @@ import PluginTab from './PluginTab';
import ChartTab from './ChartTab'; import ChartTab from './ChartTab';
import MarkdownEditorTab from './MarkdownEditorTab'; import MarkdownEditorTab from './MarkdownEditorTab';
import MarkdownViewTab from './MarkdownViewTab'; import MarkdownViewTab from './MarkdownViewTab';
import MarkdownPreviewTab from './MarkdownPreviewTab';
export default { export default {
TableDataTab, TableDataTab,
@@ -24,4 +25,5 @@ export default {
ChartTab, ChartTab,
MarkdownEditorTab, MarkdownEditorTab,
MarkdownViewTab, MarkdownViewTab,
MarkdownPreviewTab,
}; };

View File

@@ -46,6 +46,12 @@ const configLoader = () => ({
reloadTrigger: 'config-changed', reloadTrigger: 'config-changed',
}); });
const markdownManifestLoader = () => ({
url: 'files/markdown-manifest',
params: {},
reloadTrigger: 'files-changed-markdown',
});
// const sqlObjectListLoader = ({ conid, database }) => ({ // const sqlObjectListLoader = ({ conid, database }) => ({
// url: 'metadata/list-objects', // url: 'metadata/list-objects',
// params: { conid, database }, // params: { conid, database },
@@ -269,3 +275,10 @@ export function getFiles(args) {
export function useFiles(args) { export function useFiles(args) {
return useCore(filesLoader, args); return useCore(filesLoader, args);
} }
export function getMarkdownManifest(args) {
return getCore(markdownManifestLoader, args);
}
export function useMarkdownManifest(args) {
return useCore(markdownManifestLoader, args);
}

View File

@@ -4,7 +4,7 @@ import localforage from 'localforage';
import { changeTab } from './common'; import { changeTab } from './common';
import { useSetOpenedTabs } from './globalState'; import { useSetOpenedTabs } from './globalState';
export default function useEditorData({ tabid, loadFromArgs = null }) { export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null }) {
const localStorageKey = `tabdata_${tabid}`; const localStorageKey = `tabdata_${tabid}`;
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const changeCounterRef = React.useRef(0); const changeCounterRef = React.useRef(0);
@@ -57,19 +57,20 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
React.useEffect(() => { React.useEffect(() => {
initialLoad(); initialLoad();
}, []); }, [reloadToken]);
const saveToStorage = React.useCallback(async () => { const saveToStorage = React.useCallback(async () => {
if (valueRef.current == null) return; if (valueRef.current == null) return;
try { try {
await localforage.setItem(localStorageKey, valueRef.current); await localforage.setItem(localStorageKey, valueRef.current);
localStorage.removeItem(localStorageKey);
savedCounterRef.current = changeCounterRef.current; savedCounterRef.current = changeCounterRef.current;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}, [localStorageKey, valueRef]); }, [localStorageKey, valueRef]);
const saveToStorageFallback = React.useCallback(() => { const saveToStorageSync = React.useCallback(() => {
if (valueRef.current == null) return; if (valueRef.current == null) return;
if (savedCounterRef.current == changeCounterRef.current) return; // all saved if (savedCounterRef.current == changeCounterRef.current) return; // all saved
// on window unload must be synchronous actions, save to local storage instead // on window unload must be synchronous actions, save to local storage instead
@@ -86,10 +87,10 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
}; };
React.useEffect(() => { React.useEffect(() => {
window.addEventListener('beforeunload', saveToStorageFallback); window.addEventListener('beforeunload', saveToStorageSync);
return () => { return () => {
saveToStorage(); saveToStorage();
window.removeEventListener('beforeunload', saveToStorageFallback); window.removeEventListener('beforeunload', saveToStorageSync);
}; };
}, []); }, []);
@@ -99,5 +100,7 @@ export default function useEditorData({ tabid, loadFromArgs = null }) {
isLoading, isLoading,
initialData: initialDataRef.current, initialData: initialDataRef.current,
errorMessage, errorMessage,
saveToStorage,
saveToStorageSync,
}; };
} }

View File

@@ -39,8 +39,7 @@ const makeStyle = (base) => styled(base)`
const ButtonInput = makeStyle(styled.input``); const ButtonInput = makeStyle(styled.input``);
const FormStyledLabelBase = makeStyle(styled.label``); const FormStyledLabelBase = makeStyle(styled.label``);
export const FormStyledLabel = styled(FormStyledLabelBase)` export const FormStyledLabel = styled(FormStyledLabelBase)``;
`;
export default function FormStyledButton({ export default function FormStyledButton({
onClick = undefined, onClick = undefined,
@@ -67,3 +66,12 @@ export default function FormStyledButton({
/> />
); );
} }
export const StyledThemedLink = styled.a`
text-decoration: none;
cursor: pointer;
color: ${(props) => props.theme.main_background_blue[7]};
&:hover {
text-decoration: underline;
}
`;

View File

@@ -4,7 +4,7 @@ import ConnectionModal from '../modals/ConnectionModal';
import styled from 'styled-components'; import styled from 'styled-components';
import ToolbarButton, { ToolbarButtonExternalImage } from './ToolbarButton'; import ToolbarButton, { ToolbarButtonExternalImage } from './ToolbarButton';
import useNewQuery from '../query/useNewQuery'; import useNewQuery from '../query/useNewQuery';
import { useConfig } from '../utility/metadataLoaders'; import { useConfig, useMarkdownManifest } from '../utility/metadataLoaders';
import { useSetOpenedTabs, useOpenedTabs, useCurrentTheme, useSetCurrentTheme } from '../utility/globalState'; import { useSetOpenedTabs, useOpenedTabs, useCurrentTheme, useSetCurrentTheme } from '../utility/globalState';
import { openNewTab } from '../utility/common'; import { openNewTab } from '../utility/common';
import useNewFreeTable from '../freetable/useNewFreeTable'; import useNewFreeTable from '../freetable/useNewFreeTable';
@@ -25,7 +25,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const newQuery = useNewQuery(); const newQuery = useNewQuery();
const newFreeTable = useNewFreeTable(); const newFreeTable = useNewFreeTable();
const config = useConfig(); const config = useConfig();
const toolbar = config.toolbar || []; // const toolbar = config.toolbar || [];
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const openedTabs = useOpenedTabs(); const openedTabs = useOpenedTabs();
const showModal = useShowModal(); const showModal = useShowModal();
@@ -33,6 +33,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const setCurrentTheme = useSetCurrentTheme(); const setCurrentTheme = useSetCurrentTheme();
const extensions = useExtensions(); const extensions = useExtensions();
const electron = getElectron(); const electron = getElectron();
const markdownManifest = useMarkdownManifest();
React.useEffect(() => { React.useEffect(() => {
window['dbgate_createNewConnection'] = modalState.open; window['dbgate_createNewConnection'] = modalState.open;
@@ -74,45 +75,45 @@ export default function ToolBar({ toolbarPortalRef }) {
}); });
}; };
function openTabFromButton(button) { function openTabFromButton(page) {
if (openedTabs.find((x) => x.tabComponent == 'InfoPageTab' && x.props && x.props.page == button.page)) { if (
openedTabs.find(
(x) => x.tabComponent == 'MarkdownViewTab' && x.props && x.props.file == page.file && x.closedTime == null
)
) {
setOpenedTabs((tabs) => setOpenedTabs((tabs) =>
tabs.map((tab) => ({ tabs.map((tab) => ({
...tab, ...tab,
selected: tab.tabComponent == 'InfoPageTab' && tab.props && tab.props.page == button.page, selected: tab.tabComponent == 'MarkdownViewTab' && tab.props && tab.props.file == page.file,
closedTime: undefined,
})) }))
); );
} else { } else {
openNewTab(setOpenedTabs, { openNewTab(setOpenedTabs, {
title: button.title, title: page.button || page.file,
tabComponent: 'InfoPageTab', tabComponent: 'MarkdownViewTab',
icon: button.icon, icon: page.icon || 'img markdown',
props: { props: {
page: button.page, file: page.file,
}, },
}); });
} }
} }
React.useEffect(() => { React.useEffect(() => {
if (config.startupPages) { for (const page of (markdownManifest || []).filter((x) => x.autorun)) {
for (const page of config.startupPages) { openTabFromButton(page);
const button = toolbar.find((x) => x.name == page);
if (button) {
openTabFromButton(button);
} }
} }, [markdownManifest]);
}
}, config && config.startupPages);
return ( return (
<ToolbarContainer> <ToolbarContainer>
<ConnectionModal modalState={modalState} /> <ConnectionModal modalState={modalState} />
{!electron && <ToolbarButtonExternalImage image="/logo192.png" onClick={showAbout} />} {!electron && <ToolbarButtonExternalImage image="/logo192.png" onClick={showAbout} />}
{toolbar.map((button) => ( {(markdownManifest || [])
<ToolbarButton key={button.name} onClick={() => openTabFromButton(button)} icon={button.icon}> .filter((x) => x.button)
{button.title} .map((x) => (
<ToolbarButton key={x.button} onClick={() => openTabFromButton(x)} icon={x.icon || 'icon markdown'}>
{x.button}
</ToolbarButton> </ToolbarButton>
))} ))}
{config.runAsPortal == false && ( {config.runAsPortal == false && (