diff --git a/fe-app-podkop/src/icons/index.ts b/fe-app-podkop/src/icons/index.ts index 73c766a..71d9625 100644 --- a/fe-app-podkop/src/icons/index.ts +++ b/fe-app-podkop/src/icons/index.ts @@ -6,3 +6,12 @@ export * from './renderCircleXIcon24'; export * from './renderCheckIcon24'; export * from './renderXIcon24'; export * from './renderTriangleAlertIcon24'; +export * from './renderPauseIcon24'; +export * from './renderPlayIcon24'; +export * from './renderRotateCcwIcon24'; +export * from './renderCircleStopIcon24'; +export * from './renderCirclePlayIcon24'; +export * from './renderCircleCheckBigIcon24'; +export * from './renderSquareChartGanttIcon24'; +export * from './renderCogIcon24'; +export * from './renderSearchIcon24'; diff --git a/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts b/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts new file mode 100644 index 0000000..e353091 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts @@ -0,0 +1,26 @@ +import { svgEl } from '../helpers'; + +export function renderCircleCheckBigIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-check-big-icon lucide-circle-check-big', + }, + [ + svgEl('path', { + d: 'M21.801 10A10 10 0 1 1 17 3.335', + }), + svgEl('path', { + d: 'm9 11 3 3L22 4', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts b/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts new file mode 100644 index 0000000..a4102f1 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts @@ -0,0 +1,28 @@ +import { svgEl } from '../helpers'; + +export function renderCirclePlayIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-play-icon lucide-circle-play', + }, + [ + svgEl('path', { + d: 'M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z', + }), + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleStopIcon24.ts b/fe-app-podkop/src/icons/renderCircleStopIcon24.ts new file mode 100644 index 0000000..bb9e614 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleStopIcon24.ts @@ -0,0 +1,32 @@ +import { svgEl } from '../helpers'; + +export function renderCircleStopIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-stop-icon lucide-circle-stop', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('rect', { + x: '9', + y: '9', + width: '6', + height: '6', + rx: '1', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCogIcon24.ts b/fe-app-podkop/src/icons/renderCogIcon24.ts new file mode 100644 index 0000000..0093bbb --- /dev/null +++ b/fe-app-podkop/src/icons/renderCogIcon24.ts @@ -0,0 +1,34 @@ +import { svgEl } from '../helpers'; + +export function renderCogIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-cog-icon lucide-cog', + }, + [ + svgEl('path', { d: 'M11 10.27 7 3.34' }), + svgEl('path', { d: 'm11 13.73-4 6.93' }), + svgEl('path', { d: 'M12 22v-2' }), + svgEl('path', { d: 'M12 2v2' }), + svgEl('path', { d: 'M14 12h8' }), + svgEl('path', { d: 'm17 20.66-1-1.73' }), + svgEl('path', { d: 'm17 3.34-1 1.73' }), + svgEl('path', { d: 'M2 12h2' }), + svgEl('path', { d: 'm20.66 17-1.73-1' }), + svgEl('path', { d: 'm20.66 7-1.73 1' }), + svgEl('path', { d: 'm3.34 17 1.73-1' }), + svgEl('path', { d: 'm3.34 7 1.73 1' }), + svgEl('circle', { cx: '12', cy: '12', r: '2' }), + svgEl('circle', { cx: '12', cy: '12', r: '8' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts b/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts index 927964d..e6ecd3d 100644 --- a/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts +++ b/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts @@ -6,8 +6,6 @@ export function renderLoaderCircleIcon24() { 'svg', { xmlns: NS, - width: '24', - height: '24', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', diff --git a/fe-app-podkop/src/icons/renderPauseIcon24.ts b/fe-app-podkop/src/icons/renderPauseIcon24.ts new file mode 100644 index 0000000..8b150c7 --- /dev/null +++ b/fe-app-podkop/src/icons/renderPauseIcon24.ts @@ -0,0 +1,34 @@ +import { svgEl } from '../helpers'; + +export function renderPauseIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-pause-icon lucide-pause', + }, + [ + svgEl('rect', { + x: '14', + y: '3', + width: '5', + height: '18', + rx: '1', + }), + svgEl('rect', { + x: '5', + y: '3', + width: '5', + height: '18', + rx: '1', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderPlayIcon24.ts b/fe-app-podkop/src/icons/renderPlayIcon24.ts new file mode 100644 index 0000000..46c161d --- /dev/null +++ b/fe-app-podkop/src/icons/renderPlayIcon24.ts @@ -0,0 +1,23 @@ +import { svgEl } from '../helpers'; + +export function renderPlayIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-play-icon lucide-play', + }, + [ + svgEl('path', { + d: 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts b/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts new file mode 100644 index 0000000..82a5d16 --- /dev/null +++ b/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts @@ -0,0 +1,26 @@ +import { svgEl } from '../helpers'; + +export function renderRotateCcwIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-rotate-ccw-icon lucide-rotate-ccw', + }, + [ + svgEl('path', { + d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', + }), + svgEl('path', { + d: 'M3 3v5h5', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderSearchIcon24.ts b/fe-app-podkop/src/icons/renderSearchIcon24.ts new file mode 100644 index 0000000..3025384 --- /dev/null +++ b/fe-app-podkop/src/icons/renderSearchIcon24.ts @@ -0,0 +1,22 @@ +import { svgEl } from '../helpers'; + +export function renderSearchIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-search-icon lucide-search', + }, + [ + svgEl('path', { d: 'm21 21-4.34-4.34' }), + svgEl('circle', { cx: '11', cy: '11', r: '8' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts b/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts new file mode 100644 index 0000000..16c500e --- /dev/null +++ b/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts @@ -0,0 +1,30 @@ +import { svgEl } from '../helpers'; + +export function renderSquareChartGanttIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt', + }, + [ + svgEl('rect', { + width: '18', + height: '18', + x: '3', + y: '3', + rx: '2', + }), + svgEl('path', { d: 'M9 8h7' }), + svgEl('path', { d: 'M8 12h6' }), + svgEl('path', { d: 'M11 16h5' }), + ], + ); +} diff --git a/fe-app-podkop/src/partials/button/renderButton.ts b/fe-app-podkop/src/partials/button/renderButton.ts new file mode 100644 index 0000000..5912cc4 --- /dev/null +++ b/fe-app-podkop/src/partials/button/renderButton.ts @@ -0,0 +1,69 @@ +import { insertIf } from '../../helpers'; +import { renderLoaderCircleIcon24 } from '../../icons'; + +interface IRenderButtonProps { + classNames?: string[]; + disabled?: boolean; + loading?: boolean; + icon?: () => SVGSVGElement; + onClick: () => void; + text: string; +} + +export function renderButton({ + classNames = [], + disabled, + loading, + onClick, + text, + icon, +}: IRenderButtonProps) { + const hasIcon = !!loading || !!icon; + + function getWrappedIcon() { + const iconWrap = E('span', { + class: 'pdk-partial-button__icon', + }); + + if (loading) { + iconWrap.appendChild(renderLoaderCircleIcon24()); + + return iconWrap; + } + + if (icon) { + iconWrap.appendChild(icon()); + + return iconWrap; + } + + return iconWrap; + } + + function getClass() { + return [ + 'btn', + 'pdk-partial-button', + ...insertIf(Boolean(disabled), ['pdk-partial-button--disabled']), + ...insertIf(Boolean(loading), ['pdk-partial-button--loading']), + ...insertIf(Boolean(hasIcon), ['pdk-partial-button--with-icon']), + ...classNames, + ] + .filter(Boolean) + .join(' '); + } + + function getDisabled() { + if (loading || disabled) { + return true; + } + + return undefined; + } + + return E( + 'button', + { class: getClass(), disabled: getDisabled(), click: onClick }, + [...insertIf(hasIcon, [getWrappedIcon()]), E('span', {}, text)], + ); +} diff --git a/fe-app-podkop/src/partials/button/styles.ts b/fe-app-podkop/src/partials/button/styles.ts new file mode 100644 index 0000000..777ef0b --- /dev/null +++ b/fe-app-podkop/src/partials/button/styles.ts @@ -0,0 +1,33 @@ +// language=CSS +export const styles = ` +.pdk-partial-button { + text-align: center; +} + +.pdk-partial-button--with-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button--loading { +} + +.pdk-partial-button--disabled { +} + +.pdk-partial-button__icon { + margin-right: 5px; +} + +.pdk-partial-button__icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button__icon svg { + width: 16px; + height: 16px; +} +`; diff --git a/fe-app-podkop/src/partials/index.ts b/fe-app-podkop/src/partials/index.ts new file mode 100644 index 0000000..3f1e135 --- /dev/null +++ b/fe-app-podkop/src/partials/index.ts @@ -0,0 +1,7 @@ +import { styles as ButtonStyles } from './button/styles'; + +export * from './button/renderButton'; + +export const PartialStyles = ` +${ButtonStyles} +`; diff --git a/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts b/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts index 37c1b37..e317584 100644 --- a/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts +++ b/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts @@ -12,10 +12,17 @@ export async function callBaseMethod( }); if (response.stdout) { - return { - success: true, - data: JSON.parse(response.stdout) as T, - }; + try { + return { + success: true, + data: JSON.parse(response.stdout) as T, + }; + } catch (_e) { + return { + success: true, + data: response.stdout as T, + }; + } } return { diff --git a/fe-app-podkop/src/podkop/methods/shell/index.ts b/fe-app-podkop/src/podkop/methods/shell/index.ts index 7719605..983712c 100644 --- a/fe-app-podkop/src/podkop/methods/shell/index.ts +++ b/fe-app-podkop/src/podkop/methods/shell/index.ts @@ -44,20 +44,15 @@ export const PodkopShellMethods = { group, proxy, ]), - restart: async () => - callBaseMethod(Podkop.AvailableMethods.RESTART), - start: async () => - callBaseMethod(Podkop.AvailableMethods.START), - stop: async () => - callBaseMethod(Podkop.AvailableMethods.STOP), - enable: async () => - callBaseMethod(Podkop.AvailableMethods.ENABLE), - disable: async () => - callBaseMethod(Podkop.AvailableMethods.DISABLE), + restart: async () => callBaseMethod(Podkop.AvailableMethods.RESTART), + start: async () => callBaseMethod(Podkop.AvailableMethods.START), + stop: async () => callBaseMethod(Podkop.AvailableMethods.STOP), + enable: async () => callBaseMethod(Podkop.AvailableMethods.ENABLE), + disable: async () => callBaseMethod(Podkop.AvailableMethods.DISABLE), globalCheck: async () => - callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), + callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), showSingBoxConfig: async () => - callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), + callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), checkLogs: async () => - callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), + callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), }; diff --git a/fe-app-podkop/src/podkop/services/store.service.ts b/fe-app-podkop/src/podkop/services/store.service.ts index 5093ba7..996630f 100644 --- a/fe-app-podkop/src/podkop/services/store.service.ts +++ b/fe-app-podkop/src/podkop/services/store.service.ts @@ -171,6 +171,16 @@ export interface StoreType { loading: boolean; }; diagnosticsChecks: Array; + diagnosticsActions: { + restart: { loading: boolean }; + start: { loading: boolean }; + stop: { loading: boolean }; + enable: { loading: boolean }; + disable: { loading: boolean }; + globalCheck: { loading: boolean }; + viewLogs: { loading: boolean }; + showSingBoxConfig: { loading: boolean }; + }; } const initialStore: StoreType = { diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts index b34bbc0..3ed5c1e 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts @@ -6,8 +6,34 @@ import { StoreType } from '../../services'; export const initialDiagnosticStore: Pick< StoreType, - 'diagnosticsChecks' | 'diagnosticsRunAction' + 'diagnosticsChecks' | 'diagnosticsRunAction' | 'diagnosticsActions' > = { + diagnosticsActions: { + restart: { + loading: false, + }, + start: { + loading: false, + }, + stop: { + loading: false, + }, + enable: { + loading: false, + }, + disable: { + loading: false, + }, + globalCheck: { + loading: false, + }, + viewLogs: { + loading: false, + }, + showSingBoxConfig: { + loading: false, + }, + }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: [ { diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts index 16e8cd9..d69c636 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts @@ -11,6 +11,7 @@ import { renderRunAction, renderSystemInfo, } from './partials'; +import { PodkopShellMethods } from '../../methods'; function renderDiagnosticsChecks() { console.log('renderDiagnosticsChecks'); @@ -44,12 +45,174 @@ function renderDiagnosticRunActionWidget() { }); } +async function handleRestart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.restart(); + } catch (e) { + console.log('handleRestart - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: false }, + }, + }); + location.reload(); + } +} + +async function handleStop() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.stop(); + } catch (e) { + console.log('handleStop - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: false }, + }, + }); + // TODO actualize dashboard + } +} + +async function handleStart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.start(); + } catch (e) { + console.log('handleStart - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: false }, + }, + }); + location.reload(); + } +} + +async function handleEnable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.enable(); + } catch (e) { + console.log('handleEnable - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: false }, + }, + }); + //TODO actualize dashboard + } +} + +async function handleDisable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.disable(); + } catch (e) { + console.log('handleDisable - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: false }, + }, + }); + //TODO actualize dashboard + } +} + function renderDiagnosticAvailableActionsWidget() { + const diagnosticsActions = store.get().diagnosticsActions; console.log('renderDiagnosticActionsWidget'); const container = document.getElementById('pdk_diagnostic-page-actions'); - const renderedActions = renderAvailableActions(); + const renderedActions = renderAvailableActions({ + restart: { + loading: diagnosticsActions.restart.loading, + visible: true, + onClick: handleRestart, + }, + start: { + loading: diagnosticsActions.start.loading, + visible: true, + onClick: handleStart, + }, + stop: { + loading: diagnosticsActions.stop.loading, + visible: true, + onClick: handleStop, + }, + enable: { + loading: diagnosticsActions.enable.loading, + visible: true, + onClick: handleEnable, + }, + disable: { + loading: diagnosticsActions.disable.loading, + visible: true, + onClick: handleDisable, + }, + globalCheck: { + loading: diagnosticsActions.globalCheck.loading, + visible: true, + onClick: () => {}, + }, + viewLogs: { + loading: diagnosticsActions.viewLogs.loading, + visible: true, + onClick: () => {}, + }, + showSingBoxConfig: { + loading: diagnosticsActions.showSingBoxConfig.loading, + visible: true, + onClick: () => {}, + }, + }); return preserveScrollForPage(() => { container!.replaceChildren(renderedActions); @@ -103,6 +266,10 @@ async function onStoreUpdate( if (diff.diagnosticsRunAction) { renderDiagnosticRunActionWidget(); } + + if (diff.diagnosticsActions) { + renderDiagnosticAvailableActionsWidget(); + } } async function runChecks() { diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts index a8e2ed0..e24f436 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts @@ -1,11 +1,113 @@ -export function renderAvailableActions() { +import { renderButton } from '../../../../partials'; +import { + renderCircleCheckBigIcon24, + renderCirclePlayIcon24, + renderCircleStopIcon24, + renderCogIcon24, + renderPauseIcon24, + renderPlayIcon24, + renderRotateCcwIcon24, + renderSquareChartGanttIcon24, +} from '../../../../icons'; +import { insertIf } from '../../../../helpers'; + +interface ActionProps { + loading: boolean; + visible: boolean; + onClick: () => void; +} + +interface IRenderAvailableActionsProps { + restart: ActionProps; + start: ActionProps; + stop: ActionProps; + enable: ActionProps; + disable: ActionProps; + globalCheck: ActionProps; + viewLogs: ActionProps; + showSingBoxConfig: ActionProps; +} + +export function renderAvailableActions({ + restart, + start, + stop, + enable, + disable, + globalCheck, + viewLogs, + showSingBoxConfig, +}: IRenderAvailableActionsProps) { return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [ E('b', {}, 'Available actions'), - E('button', { class: 'btn' }, 'Restart podkop'), - E('button', { class: 'btn' }, 'Stop podkop'), - E('button', { class: 'btn' }, 'Disable podkop'), - E('button', { class: 'btn' }, 'Get global check'), - E('button', { class: 'btn' }, 'View logs'), - E('button', { class: 'btn' }, 'Show sing-box config'), + ...insertIf(restart.visible, [ + renderButton({ + classNames: ['cbi-button-apply'], + onClick: restart.onClick, + icon: renderRotateCcwIcon24, + text: 'Restart podkop', + loading: restart.loading, + }), + ]), + ...insertIf(stop.visible, [ + renderButton({ + classNames: ['cbi-button-remove'], + onClick: stop.onClick, + icon: renderCircleStopIcon24, + text: 'Stop podkop', + loading: stop.loading, + }), + ]), + ...insertIf(start.visible, [ + renderButton({ + classNames: ['cbi-button-save'], + onClick: start.onClick, + icon: renderCirclePlayIcon24, + text: 'Start podkop', + loading: start.loading, + }), + ]), + ...insertIf(disable.visible, [ + renderButton({ + classNames: ['cbi-button-remove'], + onClick: disable.onClick, + icon: renderPauseIcon24, + text: 'Disable podkop', + loading: disable.loading, + }), + ]), + ...insertIf(enable.visible, [ + renderButton({ + classNames: ['cbi-button-save'], + onClick: enable.onClick, + icon: renderPlayIcon24, + text: 'Enable podkop', + loading: enable.loading, + }), + ]), + ...insertIf(globalCheck.visible, [ + renderButton({ + onClick: globalCheck.onClick, + icon: renderCircleCheckBigIcon24, + text: 'Get global check', + loading: globalCheck.loading, + }), + ]), + ...insertIf(viewLogs.visible, [ + renderButton({ + onClick: viewLogs.onClick, + icon: renderSquareChartGanttIcon24, + text: 'View logs', + loading: viewLogs.loading, + }), + ]), + ...insertIf(showSingBoxConfig.visible, [ + renderButton({ + onClick: showSingBoxConfig.onClick, + icon: renderCogIcon24, + text: 'Show sing-box config', + loading: showSingBoxConfig.loading, + }), + ]), ]); } diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts index cada852..4c9ee69 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts @@ -1,3 +1,6 @@ +import { renderButton } from '../../../../partials'; +import { renderSearchIcon24 } from '../../../../icons'; + interface IRenderDiagnosticRunActionProps { loading: boolean; click: () => void; @@ -8,10 +11,12 @@ export function renderRunAction({ click, }: IRenderDiagnosticRunActionProps) { return E('div', { class: 'pdk_diagnostic-page__run_check_wrapper' }, [ - E( - 'button', - { class: 'btn', disabled: loading ? true : undefined, click }, - loading ? _('Running... please wait') : _('Run Diagnostic'), - ), + renderButton({ + text: 'Run Diagnostic', + onClick: click, + icon: renderSearchIcon24, + loading, + classNames: ['cbi-button-apply'], + }), ]); } diff --git a/fe-app-podkop/src/styles.ts b/fe-app-podkop/src/styles.ts index 2518fd8..5d562e9 100644 --- a/fe-app-podkop/src/styles.ts +++ b/fe-app-podkop/src/styles.ts @@ -1,9 +1,11 @@ // language=CSS import { DashboardTab, DiagnosticTab } from './podkop'; +import { PartialStyles } from './partials'; export const GlobalStyles = ` ${DashboardTab.styles} ${DiagnosticTab.styles} +${PartialStyles} /* Hide extra H3 for settings tab */ diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 3d833eb..6e85e56 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -411,10 +411,17 @@ async function callBaseMethod(method, args = []) { timeout: 1e4 }); if (response.stdout) { - return { - success: true, - data: JSON.parse(response.stdout) - }; + try { + return { + success: true, + data: JSON.parse(response.stdout) + }; + } catch (_e) { + return { + success: true, + data: response.stdout + }; + } } return { success: false, @@ -895,6 +902,32 @@ var DIAGNOSTICS_CHECKS_MAP = { // src/podkop/tabs/diagnostic/diagnostic.store.ts var initialDiagnosticStore = { + diagnosticsActions: { + restart: { + loading: false + }, + start: { + loading: false + }, + stop: { + loading: false + }, + enable: { + loading: false + }, + disable: { + loading: false + }, + globalCheck: { + loading: false + }, + viewLogs: { + loading: false + }, + showSingBoxConfig: { + loading: false + } + }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: [ { @@ -2283,19 +2316,40 @@ async function runFakeIPCheck() { }); } -// src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts -function renderAvailableActions() { - return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ - E("b", {}, "Available actions"), - E("button", { class: "btn" }, "Restart podkop"), - E("button", { class: "btn" }, "Stop podkop"), - E("button", { class: "btn" }, "Disable podkop"), - E("button", { class: "btn" }, "Get global check"), - E("button", { class: "btn" }, "View logs"), - E("button", { class: "btn" }, "Show sing-box config") - ]); +// src/partials/button/styles.ts +var styles2 = ` +.pdk-partial-button { + text-align: center; } +.pdk-partial-button--with-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button--loading { +} + +.pdk-partial-button--disabled { +} + +.pdk-partial-button__icon { + margin-right: 5px; +} + +.pdk-partial-button__icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button__icon svg { + width: 16px; + height: 16px; +} +`; + // src/icons/renderLoaderCircleIcon24.ts function renderLoaderCircleIcon24() { const NS = "http://www.w3.org/2000/svg"; @@ -2303,8 +2357,6 @@ function renderLoaderCircleIcon24() { "svg", { xmlns: NS, - width: "24", - height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", @@ -2532,6 +2584,398 @@ function renderTriangleAlertIcon24() { ); } +// src/icons/renderPauseIcon24.ts +function renderPauseIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-pause-icon lucide-pause" + }, + [ + svgEl("rect", { + x: "14", + y: "3", + width: "5", + height: "18", + rx: "1" + }), + svgEl("rect", { + x: "5", + y: "3", + width: "5", + height: "18", + rx: "1" + }) + ] + ); +} + +// src/icons/renderPlayIcon24.ts +function renderPlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-play-icon lucide-play" + }, + [ + svgEl("path", { + d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" + }) + ] + ); +} + +// src/icons/renderRotateCcwIcon24.ts +function renderRotateCcwIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" + }, + [ + svgEl("path", { + d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" + }), + svgEl("path", { + d: "M3 3v5h5" + }) + ] + ); +} + +// src/icons/renderCircleStopIcon24.ts +function renderCircleStopIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-stop-icon lucide-circle-stop" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("rect", { + x: "9", + y: "9", + width: "6", + height: "6", + rx: "1" + }) + ] + ); +} + +// src/icons/renderCirclePlayIcon24.ts +function renderCirclePlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-play-icon lucide-circle-play" + }, + [ + svgEl("path", { + d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" + }), + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }) + ] + ); +} + +// src/icons/renderCircleCheckBigIcon24.ts +function renderCircleCheckBigIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" + }, + [ + svgEl("path", { + d: "M21.801 10A10 10 0 1 1 17 3.335" + }), + svgEl("path", { + d: "m9 11 3 3L22 4" + }) + ] + ); +} + +// src/icons/renderSquareChartGanttIcon24.ts +function renderSquareChartGanttIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" + }, + [ + svgEl("rect", { + width: "18", + height: "18", + x: "3", + y: "3", + rx: "2" + }), + svgEl("path", { d: "M9 8h7" }), + svgEl("path", { d: "M8 12h6" }), + svgEl("path", { d: "M11 16h5" }) + ] + ); +} + +// src/icons/renderCogIcon24.ts +function renderCogIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-cog-icon lucide-cog" + }, + [ + svgEl("path", { d: "M11 10.27 7 3.34" }), + svgEl("path", { d: "m11 13.73-4 6.93" }), + svgEl("path", { d: "M12 22v-2" }), + svgEl("path", { d: "M12 2v2" }), + svgEl("path", { d: "M14 12h8" }), + svgEl("path", { d: "m17 20.66-1-1.73" }), + svgEl("path", { d: "m17 3.34-1 1.73" }), + svgEl("path", { d: "M2 12h2" }), + svgEl("path", { d: "m20.66 17-1.73-1" }), + svgEl("path", { d: "m20.66 7-1.73 1" }), + svgEl("path", { d: "m3.34 17 1.73-1" }), + svgEl("path", { d: "m3.34 7 1.73 1" }), + svgEl("circle", { cx: "12", cy: "12", r: "2" }), + svgEl("circle", { cx: "12", cy: "12", r: "8" }) + ] + ); +} + +// src/icons/renderSearchIcon24.ts +function renderSearchIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-search-icon lucide-search" + }, + [ + svgEl("path", { d: "m21 21-4.34-4.34" }), + svgEl("circle", { cx: "11", cy: "11", r: "8" }) + ] + ); +} + +// src/partials/button/renderButton.ts +function renderButton({ + classNames = [], + disabled, + loading, + onClick, + text, + icon +}) { + const hasIcon = !!loading || !!icon; + function getWrappedIcon() { + const iconWrap = E("span", { + class: "pdk-partial-button__icon" + }); + if (loading) { + iconWrap.appendChild(renderLoaderCircleIcon24()); + return iconWrap; + } + if (icon) { + iconWrap.appendChild(icon()); + return iconWrap; + } + return iconWrap; + } + function getClass() { + return [ + "btn", + "pdk-partial-button", + ...insertIf(Boolean(disabled), ["pdk-partial-button--disabled"]), + ...insertIf(Boolean(loading), ["pdk-partial-button--loading"]), + ...insertIf(Boolean(hasIcon), ["pdk-partial-button--with-icon"]), + ...classNames + ].filter(Boolean).join(" "); + } + function getDisabled() { + if (loading || disabled) { + return true; + } + return void 0; + } + return E( + "button", + { class: getClass(), disabled: getDisabled(), click: onClick }, + [...insertIf(hasIcon, [getWrappedIcon()]), E("span", {}, text)] + ); +} + +// src/partials/index.ts +var PartialStyles = ` +${styles2} +`; + +// src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +function renderAvailableActions({ + restart, + start, + stop, + enable, + disable, + globalCheck, + viewLogs, + showSingBoxConfig +}) { + return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ + E("b", {}, "Available actions"), + ...insertIf(restart.visible, [ + renderButton({ + classNames: ["cbi-button-apply"], + onClick: restart.onClick, + icon: renderRotateCcwIcon24, + text: "Restart podkop", + loading: restart.loading + }) + ]), + ...insertIf(stop.visible, [ + renderButton({ + classNames: ["cbi-button-remove"], + onClick: stop.onClick, + icon: renderCircleStopIcon24, + text: "Stop podkop", + loading: stop.loading + }) + ]), + ...insertIf(start.visible, [ + renderButton({ + classNames: ["cbi-button-save"], + onClick: start.onClick, + icon: renderCirclePlayIcon24, + text: "Start podkop", + loading: start.loading + }) + ]), + ...insertIf(disable.visible, [ + renderButton({ + classNames: ["cbi-button-remove"], + onClick: disable.onClick, + icon: renderPauseIcon24, + text: "Disable podkop", + loading: disable.loading + }) + ]), + ...insertIf(enable.visible, [ + renderButton({ + classNames: ["cbi-button-save"], + onClick: enable.onClick, + icon: renderPlayIcon24, + text: "Enable podkop", + loading: enable.loading + }) + ]), + ...insertIf(globalCheck.visible, [ + renderButton({ + onClick: globalCheck.onClick, + icon: renderCircleCheckBigIcon24, + text: "Get global check", + loading: globalCheck.loading + }) + ]), + ...insertIf(viewLogs.visible, [ + renderButton({ + onClick: viewLogs.onClick, + icon: renderSquareChartGanttIcon24, + text: "View logs", + loading: viewLogs.loading + }) + ]), + ...insertIf(showSingBoxConfig.visible, [ + renderButton({ + onClick: showSingBoxConfig.onClick, + icon: renderCogIcon24, + text: "Show sing-box config", + loading: showSingBoxConfig.loading + }) + ]) + ]); +} + // src/podkop/tabs/diagnostic/partials/renderCheckSection.ts function renderCheckSummary(items) { if (!items.length) { @@ -2693,11 +3137,13 @@ function renderRunAction({ click }) { return E("div", { class: "pdk_diagnostic-page__run_check_wrapper" }, [ - E( - "button", - { class: "btn", disabled: loading ? true : void 0, click }, - loading ? _("Running... please wait") : _("Run Diagnostic") - ) + renderButton({ + text: "Run Diagnostic", + onClick: click, + icon: renderSearchIcon24, + loading, + classNames: ["cbi-button-apply"] + }) ]); } @@ -2742,10 +3188,162 @@ function renderDiagnosticRunActionWidget() { container.replaceChildren(renderedAction); }); } +async function handleRestart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: true } + } + }); + try { + await PodkopShellMethods.restart(); + } catch (e) { + console.log("handleRestart - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: false } + } + }); + location.reload(); + } +} +async function handleStop() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: true } + } + }); + try { + await PodkopShellMethods.stop(); + } catch (e) { + console.log("handleStop - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: false } + } + }); + } +} +async function handleStart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: true } + } + }); + try { + await PodkopShellMethods.start(); + } catch (e) { + console.log("handleStart - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: false } + } + }); + location.reload(); + } +} +async function handleEnable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: true } + } + }); + try { + await PodkopShellMethods.enable(); + } catch (e) { + console.log("handleEnable - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: false } + } + }); + } +} +async function handleDisable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: true } + } + }); + try { + await PodkopShellMethods.disable(); + } catch (e) { + console.log("handleDisable - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: false } + } + }); + } +} function renderDiagnosticAvailableActionsWidget() { + const diagnosticsActions = store.get().diagnosticsActions; console.log("renderDiagnosticActionsWidget"); const container = document.getElementById("pdk_diagnostic-page-actions"); - const renderedActions = renderAvailableActions(); + const renderedActions = renderAvailableActions({ + restart: { + loading: diagnosticsActions.restart.loading, + visible: true, + onClick: handleRestart + }, + start: { + loading: diagnosticsActions.start.loading, + visible: true, + onClick: handleStart + }, + stop: { + loading: diagnosticsActions.stop.loading, + visible: true, + onClick: handleStop + }, + enable: { + loading: diagnosticsActions.enable.loading, + visible: true, + onClick: handleEnable + }, + disable: { + loading: diagnosticsActions.disable.loading, + visible: true, + onClick: handleDisable + }, + globalCheck: { + loading: diagnosticsActions.globalCheck.loading, + visible: true, + onClick: () => { + } + }, + viewLogs: { + loading: diagnosticsActions.viewLogs.loading, + visible: true, + onClick: () => { + } + }, + showSingBoxConfig: { + loading: diagnosticsActions.showSingBoxConfig.loading, + visible: true, + onClick: () => { + } + } + }); return preserveScrollForPage(() => { container.replaceChildren(renderedActions); }); @@ -2761,7 +3359,7 @@ function renderDiagnosticSystemInfoWidget() { }, { key: "Luci App", - value: PODKOP_LUCI_APP_VERSION + value: "1" }, { key: "Sing-box", @@ -2788,6 +3386,9 @@ async function onStoreUpdate2(next, prev, diff) { if (diff.diagnosticsRunAction) { renderDiagnosticRunActionWidget(); } + if (diff.diagnosticsActions) { + renderDiagnosticAvailableActionsWidget(); + } } async function runChecks() { try { @@ -2805,8 +3406,6 @@ async function runChecks() { store.set({ diagnosticsRunAction: { loading: false } }); } } -async function test() { -} async function initController2() { onMount("diagnostic-status").then(() => { console.log("diagnostic controller initialized."); @@ -2816,12 +3415,11 @@ async function initController2() { renderDiagnosticRunActionWidget(); renderDiagnosticAvailableActionsWidget(); renderDiagnosticSystemInfoWidget(); - test(); }); } // src/podkop/tabs/diagnostic/styles.ts -var styles2 = ` +var styles3 = ` #cbi-podkop-diagnostic-_mount_node > div { width: 100%; @@ -2967,13 +3565,14 @@ var styles2 = ` var DiagnosticTab = { render: render2, initController: initController2, - styles: styles2 + styles: styles3 }; // src/styles.ts var GlobalStyles = ` ${DashboardTab.styles} ${DiagnosticTab.styles} +${PartialStyles} /* Hide extra H3 for settings tab */