mirror of
https://github.com/kmvan/x-prober.git
synced 2026-04-23 10:39:01 +08:00
completed modules
This commit is contained in:
37
src/Components/Module/components/arrow.module.scss
Normal file
37
src/Components/Module/components/arrow.module.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
:root {
|
||||
--x-card-legend-arrow-fg: var(--x-card-legend-fg);
|
||||
--x-card-legend-arrow-bg-hover: hsl(0 0% 0% / 0.05);
|
||||
--x-card-legend-arrow-bg-active: hsl(0 0% 0% / 0.1);
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--x-card-legend-arrow-fg: var(--x-card-legend-fg);
|
||||
--x-card-legend-arrow-bg-hover: hsl(0 0% 100% / 0.05);
|
||||
--x-card-legend-arrow-bg-active: hsl(0 0% 100% / 0.1);
|
||||
}
|
||||
}
|
||||
.arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: var(--x-radius);
|
||||
background: transparent;
|
||||
padding: var(--x-gutter-sm);
|
||||
color: var(--x-card-legend-arrow-fg);
|
||||
&:hover {
|
||||
background: var(--x-card-legend-arrow-bg-hover);
|
||||
color: var(--x-card-legend-arrow-fg);
|
||||
}
|
||||
&:active {
|
||||
background: var(--x-card-legend-arrow-bg-active);
|
||||
color: var(--x-card-legend-arrow-fg);
|
||||
}
|
||||
&[data-disabled],
|
||||
&[data-disabled]:hover {
|
||||
opacity: 0.1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
38
src/Components/Module/components/arrow.tsx
Normal file
38
src/Components/Module/components/arrow.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { type FC, type MouseEvent, useCallback } from 'react';
|
||||
import { gettext } from '@/Components/Language/index.ts';
|
||||
import styles from './arrow.module.scss';
|
||||
import { ModuleStore } from './store.ts';
|
||||
export const ModuleArrow: FC<{
|
||||
isDown: boolean;
|
||||
id: string;
|
||||
}> = observer(({ isDown, id }) => {
|
||||
const { disabledMoveUpId, disabledMoveDownId, moveUp, moveDown } =
|
||||
ModuleStore;
|
||||
const disabled = isDown ? disabledMoveDownId === id : disabledMoveUpId === id;
|
||||
const handleMove = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isDown) {
|
||||
moveDown(id);
|
||||
return;
|
||||
}
|
||||
moveUp(id);
|
||||
},
|
||||
[isDown, moveDown, moveUp, id]
|
||||
);
|
||||
return (
|
||||
<button
|
||||
className={styles.arrow}
|
||||
data-disabled={disabled || undefined}
|
||||
disabled={disabled}
|
||||
onClick={handleMove}
|
||||
title={isDown ? gettext('Move down') : gettext('Move up')}
|
||||
type="button"
|
||||
>
|
||||
{isDown ? <ChevronDown /> : <ChevronUp />}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
36
src/Components/Module/components/group.module.scss
Normal file
36
src/Components/Module/components/group.module.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
:root {
|
||||
--x-card-group-label-fg: var(--x-fg);
|
||||
--x-card-group-split-color: hsl(0 0% 0% / 0.1);
|
||||
--x-card-group-bg-hover: hsl(0 0% 0% / 0.05);
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--x-card-group-label-fg: var(--x-fg);
|
||||
--x-card-group-split-color: hsl(0 0% 100% / 0.1);
|
||||
--x-card-group-bg-hover: hsl(0 0% 100% / 0.05);
|
||||
}
|
||||
}
|
||||
.main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(5rem, 10rem) 1fr;
|
||||
gap: var(--x-gutter-sm);
|
||||
border-radius: var(--x-radius);
|
||||
&:hover {
|
||||
background: var(--x-card-group-bg-hover);
|
||||
}
|
||||
}
|
||||
.label {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
color: var(--x-card-group-label-fg);
|
||||
font-family: var(--x-text-font-family);
|
||||
text-align: right;
|
||||
word-break: normal;
|
||||
&::after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
// align-items: center;
|
||||
gap: var(--x-gutter-sm);
|
||||
}
|
||||
16
src/Components/Module/components/group.tsx
Normal file
16
src/Components/Module/components/group.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import styles from './group.module.scss';
|
||||
export const ModuleGroup: FC<{
|
||||
label?: ReactNode;
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
}> = ({ label = '', title = '', children }) => (
|
||||
<div className={styles.main}>
|
||||
{Boolean(label) && (
|
||||
<div className={styles.label} title={title}>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
5
src/Components/Module/components/index.module.scss
Normal file
5
src/Components/Module/components/index.module.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--x-gutter);
|
||||
padding: 0 var(--x-gutter);
|
||||
}
|
||||
34
src/Components/Module/components/index.tsx
Normal file
34
src/Components/Module/components/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { type FC, useEffect } from 'react';
|
||||
import { ModulePriority } from '@/Components/Module/components/priority.ts';
|
||||
import styles from './index.module.scss';
|
||||
import { ModulePreset } from './preset.ts';
|
||||
import { ModuleStorage } from './storage.ts';
|
||||
import { ModuleStore } from './store.ts';
|
||||
import type { SortedModuleProps } from './typings.ts';
|
||||
export const Modules: FC = observer(() => {
|
||||
const { setSortedModules, availableModules } = ModuleStore;
|
||||
useEffect(() => {
|
||||
const storageItems = ModuleStorage.getItems();
|
||||
const sorted: SortedModuleProps[] = [];
|
||||
for (const preset of ModulePreset.items) {
|
||||
sorted.push({
|
||||
id: preset.id,
|
||||
priority:
|
||||
Number(storageItems?.[preset.id]) ||
|
||||
ModulePriority.indexOf(preset.id),
|
||||
});
|
||||
}
|
||||
setSortedModules(sorted);
|
||||
}, [setSortedModules]);
|
||||
if (!availableModules.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{availableModules.map(({ id, content: C }) => {
|
||||
return <C key={id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
51
src/Components/Module/components/item.module.scss
Normal file
51
src/Components/Module/components/item.module.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
@use "../../Style/components/device.scss" as m;
|
||||
:root {
|
||||
--x-module-bg: hsl(0 0% 0% / 0.95);
|
||||
--x-module-header-bg: hsl(0 0% 100% / 0.75);
|
||||
--x-module-header-fg: hsl(0 0% 0%);
|
||||
--x-module-header-title-fg: hsl(0 0% 0% / 0.7);
|
||||
--x-module-header-title-bg: hsl(0 0% 0% / 0.1);
|
||||
--x-module-body-bg: var(--x-module-header-bg);
|
||||
--x-module-box-shadow: hsla(0 0% 20% 0.3) 0px -1px 0px hsl(0 0% 100%) 0px 1px 0px inset,
|
||||
hsla(0 0% 20% 0.3) 0px -1px 0px inset hsl(0 0% 100%) 0px 1px 0px;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--x-module-bg: hsl(0 0% 15% / 0.95);
|
||||
--x-module-header-bg: hsl(0 0% 100% / 0.1);
|
||||
--x-module-header-fg: hsl(0 0% 100% / 0.7);
|
||||
--x-module-header-title-fg: hsl(0 0% 100% / 0.7);
|
||||
--x-module-header-title-bg: hsl(0 0% 100% / 0.1);
|
||||
--x-module-body-bg: var(--x-module-header-bg);
|
||||
--x-module-box-shadow: 0px 0px 0px 1px hsl(0 0% 0%) inset;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
scroll-margin-top: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// z-index: 10;
|
||||
border-radius: var(--x-radius) var(--x-radius) 0 0;
|
||||
background: var(--x-module-header-bg);
|
||||
// backdrop-filter: blur(5px);
|
||||
// background: var(--x-module-header-bg);
|
||||
padding: 1px;
|
||||
// position: sticky;
|
||||
// top: 0;
|
||||
width: fit-content;
|
||||
color: var(--x-module-header-fg);
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title {
|
||||
font-weight: normal;
|
||||
}
|
||||
.body {
|
||||
display: grid;
|
||||
gap: var(--x-gutter-sm);
|
||||
border-radius: 0 var(--x-radius) var(--x-radius) var(--x-radius);
|
||||
background: var(--x-module-body-bg);
|
||||
padding: var(--x-gutter);
|
||||
}
|
||||
28
src/Components/Module/components/item.tsx
Normal file
28
src/Components/Module/components/item.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { ModuleArrow } from '@/Components/Module/components/arrow.tsx';
|
||||
import styles from './item.module.scss';
|
||||
|
||||
const ModuleItemTitle: FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
return (
|
||||
<h2 className={styles.header}>
|
||||
<ModuleArrow id={id} isDown={false} />
|
||||
<span className={styles.title}>{title}</span>
|
||||
<ModuleArrow id={id} isDown />
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
export const ModuleItem: FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}> = ({ id, title, children, ...props }) => {
|
||||
return (
|
||||
<div className={styles.main} id={id} {...props}>
|
||||
<ModuleItemTitle id={id} title={title} />
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
29
src/Components/Module/components/preset.ts
Normal file
29
src/Components/Module/components/preset.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { DatabaseLoader } from '@/Components/Database/components/loader.ts';
|
||||
import { DiskUsageLoader } from '@/Components/DiskUsage/components/loader.ts';
|
||||
import { MyInfoLoader } from '@/Components/MyInfo/components/loader.ts';
|
||||
import { NetworkStatsLoader } from '@/Components/NetworkStats/components/loader.ts';
|
||||
import { NodesLoader } from '@/Components/Nodes/components/loader.ts';
|
||||
import { PhpExtensionsLoader } from '@/Components/PhpExtensions/components/loader.ts';
|
||||
import { PhpInfoLoader } from '@/Components/PhpInfo/components/loader.ts';
|
||||
import { PingLoader } from '@/Components/Ping/components/loader.ts';
|
||||
import { ServerBenchmarkLoader } from '@/Components/ServerBenchmark/components/loader.ts';
|
||||
import { ServerInfoLoader } from '@/Components/ServerInfo/components/loader.ts';
|
||||
import { ServerStatusLoader } from '@/Components/ServerStatus/components/loader.ts';
|
||||
import { TemperatureSensorLoader } from '@/Components/TemperatureSensor/components/loader.ts';
|
||||
|
||||
export const ModulePreset = {
|
||||
items: [
|
||||
NodesLoader,
|
||||
TemperatureSensorLoader,
|
||||
ServerStatusLoader,
|
||||
NetworkStatsLoader,
|
||||
DiskUsageLoader,
|
||||
PingLoader,
|
||||
ServerInfoLoader,
|
||||
PhpInfoLoader,
|
||||
PhpExtensionsLoader,
|
||||
DatabaseLoader,
|
||||
MyInfoLoader,
|
||||
ServerBenchmarkLoader,
|
||||
],
|
||||
};
|
||||
27
src/Components/Module/components/priority.ts
Normal file
27
src/Components/Module/components/priority.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { PingConstants } from '@/Components/Ping/components/constants.ts';
|
||||
import { DatabaseConstants } from '../../Database/components/constants.ts';
|
||||
import { DiskUsageConstants } from '../../DiskUsage/components/constants.ts';
|
||||
import { MyInfoConstants } from '../../MyInfo/components/constants.ts';
|
||||
import { NetworkStatsConstants } from '../../NetworkStats/components/constants.ts';
|
||||
import { NodesConstants } from '../../Nodes/components/constants.ts';
|
||||
import { PhpExtensionsConstants } from '../../PhpExtensions/components/constants.ts';
|
||||
import { PhpInfoConstants } from '../../PhpInfo/components/constants.ts';
|
||||
import { ServerBenchmarkConstants } from '../../ServerBenchmark/components/constants.ts';
|
||||
import { ServerInfoConstants } from '../../ServerInfo/components/constants.ts';
|
||||
import { ServerStatusConstants } from '../../ServerStatus/components/constants.ts';
|
||||
import { TemperatureSensorConstants } from '../../TemperatureSensor/components/constants.ts';
|
||||
|
||||
export const ModulePriority = [
|
||||
NodesConstants.id,
|
||||
TemperatureSensorConstants.id,
|
||||
ServerStatusConstants.id,
|
||||
NetworkStatsConstants.id,
|
||||
DiskUsageConstants.id,
|
||||
ServerInfoConstants.id,
|
||||
PingConstants.id,
|
||||
PhpInfoConstants.id,
|
||||
PhpExtensionsConstants.id,
|
||||
DatabaseConstants.id,
|
||||
ServerBenchmarkConstants.id,
|
||||
MyInfoConstants.id,
|
||||
];
|
||||
27
src/Components/Module/components/storage.ts
Normal file
27
src/Components/Module/components/storage.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { StoragePriorityItemProps } from './store.ts';
|
||||
|
||||
const STORAGE_KEY = 'module-priority';
|
||||
export const ModuleStorage = {
|
||||
getItems(): Record<string, number> {
|
||||
const items = localStorage.getItem(STORAGE_KEY);
|
||||
if (!items) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(items) as Record<string, number>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
setItems(items: Record<string, number>) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
||||
},
|
||||
getPriority(id: string): number {
|
||||
return this.getItems()[id] || 0;
|
||||
},
|
||||
setPriority({ id, priority }: StoragePriorityItemProps) {
|
||||
const items = this.getItems();
|
||||
items[id] = priority;
|
||||
this.setItems(items);
|
||||
},
|
||||
};
|
||||
84
src/Components/Module/components/store.ts
Normal file
84
src/Components/Module/components/store.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { configure, makeAutoObservable } from 'mobx';
|
||||
import { ModulePriority } from '@/Components/Module/components/priority.ts';
|
||||
import { PollStore } from '@/Components/Poll/components/store.ts';
|
||||
import type { PollDataProps } from '@/Components/Poll/components/typings.ts';
|
||||
import { ModulePreset } from './preset.ts';
|
||||
import { ModuleStorage } from './storage.ts';
|
||||
import type { ModuleProps, SortedModuleProps } from './typings.ts';
|
||||
|
||||
configure({
|
||||
enforceActions: 'observed',
|
||||
});
|
||||
export interface StoragePriorityItemProps {
|
||||
id: string;
|
||||
priority: number;
|
||||
}
|
||||
const saveSortedStorage = (items: SortedModuleProps[]) => {
|
||||
const sorted: Record<string, number> = {};
|
||||
for (const item of items) {
|
||||
sorted[item.id] = item.priority;
|
||||
}
|
||||
ModuleStorage.setItems(sorted);
|
||||
};
|
||||
class Main {
|
||||
sortedModules: SortedModuleProps[] = [];
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
setSortedModules = (modules: SortedModuleProps[]) => {
|
||||
this.sortedModules = modules.toSorted((a, b) => {
|
||||
return a.priority - b.priority;
|
||||
});
|
||||
};
|
||||
get availableModules(): ModuleProps[] {
|
||||
const { pollData } = PollStore;
|
||||
const items = ModulePreset.items
|
||||
.filter(({ id }) => Boolean(pollData?.[id as keyof PollDataProps]))
|
||||
.toSorted((a, b) => {
|
||||
const moduleA = this.sortedModules.find((item) => item.id === a.id);
|
||||
const moduleB = this.sortedModules.find((item) => item.id === b.id);
|
||||
return (
|
||||
Number(moduleA?.priority ?? ModulePriority.indexOf(a.id)) -
|
||||
Number(moduleB?.priority ?? ModulePriority.indexOf(b.id))
|
||||
);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
moveUp = (id: string) => {
|
||||
const i = this.sortedModules.findIndex((item) => item.id === id);
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
[this.sortedModules[i].priority, this.sortedModules[i - 1].priority] = [
|
||||
this.sortedModules[i - 1].priority,
|
||||
this.sortedModules[i].priority,
|
||||
];
|
||||
saveSortedStorage(this.sortedModules);
|
||||
};
|
||||
moveDown = (id: string) => {
|
||||
const i = this.sortedModules.findIndex((item) => item.id === id);
|
||||
if (i === this.sortedModules.length - 1) {
|
||||
return;
|
||||
}
|
||||
[this.sortedModules[i].priority, this.sortedModules[i + 1].priority] = [
|
||||
this.sortedModules[i + 1].priority,
|
||||
this.sortedModules[i].priority,
|
||||
];
|
||||
saveSortedStorage(this.sortedModules);
|
||||
};
|
||||
get disabledMoveUpId(): string {
|
||||
const items = this.availableModules;
|
||||
if (items.length <= 1) {
|
||||
return '';
|
||||
}
|
||||
return items[0].id;
|
||||
}
|
||||
get disabledMoveDownId(): string {
|
||||
const items = this.availableModules;
|
||||
if (items.length <= 1) {
|
||||
return '';
|
||||
}
|
||||
return items.at(-1)?.id ?? '';
|
||||
}
|
||||
}
|
||||
export const ModuleStore = new Main();
|
||||
11
src/Components/Module/components/typings.ts
Normal file
11
src/Components/Module/components/typings.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
export interface ModuleProps {
|
||||
id: string;
|
||||
content: FC;
|
||||
nav: FC;
|
||||
}
|
||||
export interface SortedModuleProps {
|
||||
id: string;
|
||||
priority: number;
|
||||
}
|
||||
Reference in New Issue
Block a user