Add new config format

This commit is contained in:
Meierschlumpf
2022-12-04 17:36:30 +01:00
parent b2f5149527
commit d5a3b3f3ba
76 changed files with 2461 additions and 1034 deletions

View File

@@ -0,0 +1,16 @@
import { DashboardDetailView } from './Views/DetailView';
import { DashboardEditView } from './Views/EditView';
import { useEditModeStore } from './Views/store';
interface DashboardProps {}
export const Dashboard = () => {
const isEditMode = useEditModeStore((x) => x.enabled);
return (
<>
{/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */}
{isEditMode ? <DashboardEditView /> : <DashboardDetailView />}
</>
);
};

View File

@@ -0,0 +1,91 @@
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
import { NextLink } from '@mantine/next';
import { createStyles } from '@mantine/styles';
import { ServiceType } from '../../../../types/service';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useEditModeStore } from '../../Views/store';
import { BaseTileProps } from '../type';
interface ServiceTileProps extends BaseTileProps {
service: ServiceType;
}
export const ServiceTile = ({ className, service }: ServiceTileProps) => {
const isEditMode = useEditModeStore((x) => x.enabled);
const { cx, classes } = useStyles();
const {
classes: { card: cardClass },
} = useCardStyles();
const inner = (
<>
<Text align="center" weight={500} size="md" className={classes.serviceName}>
{service.name}
</Text>
<Center style={{ height: '75%', flex: 1 }}>
<img className={classes.image} src={service.appearance.iconUrl} alt="" />
</Center>
</>
);
return (
<Card className={cx(className, cardClass)} withBorder radius="lg" shadow="md">
{isEditMode &&
{
/*<AppShelfMenu service={service} />*/
}}{' '}
{/* TODO: change to serviceMenu */}
{!service.url || isEditMode ? (
<UnstyledButton
className={classes.button}
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
>
{inner}
</UnstyledButton>
) : (
<UnstyledButton
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
component={NextLink}
href={service.url}
target={service.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={cx(classes.button, classes.link)}
>
{inner}
</UnstyledButton>
)}
{/*<ServicePing service={service} />*/}
</Card>
);
};
const useStyles = createStyles((theme, _params, getRef) => {
return {
image: {
ref: getRef('image'),
maxHeight: '80%',
maxWidth: '80%',
transition: 'transform 100ms ease-in-out',
},
serviceName: {
ref: getRef('serviceName'),
},
button: {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
},
link: {
[`&:hover .${getRef('image')}`]: {
// TODO: add styles for image when hovering card
},
[`&:hover .${getRef('serviceName')}`]: {
// TODO: add styles for service name when hovering card
},
},
};
});

View File

@@ -0,0 +1,48 @@
import { ReactNode, RefObject } from 'react';
interface GridstackTileWrapperProps {
id: string;
type: 'service' | 'module';
x?: number;
y?: number;
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
itemRef: RefObject<HTMLDivElement>;
children: ReactNode;
}
export const GridstackTileWrapper = ({
id,
type,
x,
y,
width,
height,
minWidth,
minHeight,
maxWidth,
maxHeight,
children,
itemRef,
}: GridstackTileWrapperProps) => (
<div
className="grid-stack-item"
data-type={type}
data-id={id}
gs-x={x}
gs-y={y}
gs-w={width}
gs-h={height}
gs-min-w={minWidth}
gs-min-h={minHeight}
gs-max-w={maxWidth}
gs-max-h={maxHeight}
ref={itemRef}
>
{children}
</div>
);

View File

@@ -0,0 +1,76 @@
import { IntegrationsType } from '../../../types/integration';
import { ServiceTile } from './Service/Service';
/*import { CalendarTile } from './calendar';
import { ClockTile } from './clock';
import { DashDotTile } from './dash-dot';
import { WeatherTile } from './weather';*/
type TileDefinitionProps = {
[key in keyof IntegrationsType | 'service']: {
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
component: React.ElementType;
};
};
// TODO: change components for other modules
export const Tiles: TileDefinitionProps = {
service: {
component: ServiceTile,
minWidth: 2,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
bitTorrent: {
component: CalendarTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
calendar: {
component: CalendarTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
clock: {
component: ClockTile,
minWidth: 4,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
dashDot: {
component: DashDotTile,
minWidth: 4,
maxWidth: 9,
minHeight: 5,
maxHeight: 14,
},
torrentNetworkTraffic: {
component: CalendarTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
useNet: {
component: CalendarTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
weather: {
component: WeatherTile,
minWidth: 4,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
};

View File

@@ -0,0 +1,3 @@
export interface BaseTileProps {
className?: string;
}

View File

@@ -0,0 +1,5 @@
import { DashboardView } from './main';
export const DashboardDetailView = () => {
return <DashboardView />;
};

View File

@@ -0,0 +1,5 @@
import { DashboardView } from './main';
export const DashboardEditView = () => {
return <DashboardView />;
};

View File

@@ -0,0 +1,39 @@
import { Group, Stack } from '@mantine/core';
import { useMemo } from 'react';
import { useConfigContext } from '../../../config/provider';
import { ServiceTile } from '../Tiles/Service/Service';
import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar';
export const DashboardView = () => {
const wrappers = useWrapperItems();
return (
<Group align="top" h="100%">
{/*<DashboardSidebar location="left" />*/}
<Stack mx={-10} style={{ flexGrow: 1 }}>
{wrappers.map(
(item) =>
item.type === 'category'
? 'category' //<DashboardCategory key={item.id} category={item as unknown as CategoryType} />
: 'wrapper' //<DashboardWrapper key={item.id} wrapper={item as WrapperType} />
)}
</Stack>
{/*<DashboardSidebar location="right" />*/}
</Group>
);
};
const useWrapperItems = () => {
const { config } = useConfigContext();
return useMemo(
() =>
config
? [
...config.categories.map((c) => ({ ...c, type: 'category' })),
...config.wrappers.map((w) => ({ ...w, type: 'wrapper' })),
].sort((a, b) => a.position - b.position)
: [],
[config?.categories, config?.wrappers]
);
};

View File

@@ -0,0 +1,11 @@
import create from 'zustand';
interface EditModeState {
enabled: boolean;
toggleEditMode: () => void;
}
export const useEditModeStore = create<EditModeState>((set) => ({
enabled: false,
toggleEditMode: () => set((state) => ({ enabled: !state.enabled })),
}));

View File

@@ -0,0 +1,72 @@
import { Card } from '@mantine/core';
import { RefObject } from 'react';
import { Tiles } from '../../Tiles/definition';
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
import { useGridstack } from '../gridstack/use-gridstack';
interface DashboardSidebarProps {
location: 'right' | 'left';
}
export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
const { refs, items, integrations } = useGridstack('sidebar', location);
const minRow = useMinRowForFullHeight(refs.wrapper);
return (
<Card
withBorder
w={300}
style={{
background: 'none',
borderStyle: 'dashed',
}}
>
<div
className="grid-stack grid-stack-sidebar"
style={{ transitionDuration: '0s', height: '100%' }}
data-sidebar={location}
gs-min-row={minRow}
ref={refs.wrapper}
>
{items.map((service) => {
const { component: TileComponent, ...tile } = Tiles['service'];
return (
<GridstackTileWrapper
id={service.id}
type="service"
key={service.id}
itemRef={refs.items.current[service.id]}
{...tile}
{...service.shape.location}
{...service.shape.size}
>
<TileComponent className="grid-stack-item-content" service={service} />
</GridstackTileWrapper>
);
})}
{Object.entries(integrations).map(([k, v]) => {
const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles];
return (
<GridstackTileWrapper
id={k}
type="module"
key={k}
itemRef={refs.items.current[k]}
{...tile}
{...v.shape.location}
{...v.shape.size}
>
<TileComponent className="grid-stack-item-content" module={v} />
</GridstackTileWrapper>
);
})}
</div>
</Card>
);
};
const useMinRowForFullHeight = (wrapperRef: RefObject<HTMLDivElement>) => {
return wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2;
};

View File

@@ -0,0 +1,68 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject } from 'react';
import { IntegrationsType } from '../../../../types/integration';
import { ServiceType } from '../../../../types/service';
export const initializeGridstack = (
areaType: 'wrapper' | 'category' | 'sidebar',
wrapperRef: RefObject<HTMLDivElement>,
gridRef: MutableRefObject<GridStack | undefined>,
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
areaId: string,
items: ServiceType[],
integrations: IntegrationsType,
isEditMode: boolean,
events: {
onChange: (changedNode: GridStackNode) => void;
onAdd: (addedNode: GridStackNode) => void;
}
) => {
if (!wrapperRef.current) return;
// calculates the currently available count of columns
const columnCount = areaType === 'sidebar' ? 4 : Math.floor(wrapperRef.current.offsetWidth / 64);
const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 64);
// initialize gridstack
gridRef.current = GridStack.init(
{
column: columnCount,
margin: 10,
cellHeight: 64,
float: true,
alwaysShowResizeHandle: 'mobile',
acceptWidgets: true,
disableOneColumnMode: true,
staticGrid: !isEditMode,
minRow,
},
// selector of the gridstack item (it's eather category or wrapper)
`.grid-stack-${areaType}[data-${areaType}='${areaId}']`
);
const grid = gridRef.current;
// Add listener for moving items around in a wrapper
grid.on('change', (_, el) => {
const nodes = el as GridStackNode[];
const firstNode = nodes.at(0);
if (!firstNode) return;
events.onChange(firstNode);
});
// Add listener for moving items in config from one wrapper to another
grid.on('added', (_, el) => {
const nodes = el as GridStackNode[];
const firstNode = nodes.at(0);
if (!firstNode) return;
events.onAdd(firstNode);
});
grid.batchUpdate();
grid.removeAll(false);
items.forEach(
({ id }) =>
itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement)
);
Object.keys(integrations).forEach(
(key) =>
itemRefs.current[key] && grid.makeWidget(itemRefs.current[key].current as HTMLDivElement)
);
grid.batchUpdate(false);
};

View File

@@ -0,0 +1,231 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import {
createRef,
LegacyRef,
MutableRefObject,
RefObject,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { useResize } from '../../../../hooks/use-resize';
import { IntegrationsType } from '../../../../types/integration';
import { ServiceType } from '../../../../types/service';
import { TileBaseType } from '../../../../types/tile';
import { useEditModeStore } from '../../Views/store';
import { initializeGridstack } from './init-gridstack';
interface UseGristackReturnType {
items: ServiceType[];
integrations: Partial<IntegrationsType>;
refs: {
wrapper: RefObject<HTMLDivElement>;
items: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>;
gridstack: MutableRefObject<GridStack | undefined>;
};
}
export const useGridstack = (
areaType: 'wrapper' | 'category' | 'sidebar',
areaId: string
): UseGristackReturnType => {
const isEditMode = useEditModeStore((x) => x.enabled);
const { config, name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
// define reference for wrapper - is used to calculate the width of the wrapper
const wrapperRef = useRef<HTMLDivElement>(null);
// references to the diffrent items contained in the gridstack
const itemRefs = useRef<Record<string, RefObject<HTMLDivElement>>>({});
// reference of the gridstack object for modifications after initialization
const gridRef = useRef<GridStack>();
// width of the wrapper (updating on page resize)
const { width, height } = useResize(wrapperRef);
const items = useMemo(
() =>
config?.services.filter(
(x) =>
x.area.type === areaType &&
(x.area.type === 'sidebar'
? x.area.properties.location === areaId
: x.area.properties.id === areaId)
) ?? [],
[config]
);
const integrations = useMemo(() => {
if (!config) return;
return (Object.entries(config.integrations) as [keyof IntegrationsType, TileBaseType][])
.filter(
([k, v]) =>
v.area.type === areaType &&
(v.area.type === 'sidebar'
? v.area.properties.location === areaId
: v.area.properties.id === areaId)
)
.reduce((prev, [k, v]) => {
prev[k] = v as unknown as any;
return prev;
}, {} as IntegrationsType);
}, [config]);
// define items in itemRefs for easy access and reference to items
if (
Object.keys(itemRefs.current).length !==
items.length + Object.keys(integrations ?? {}).length
) {
items.forEach(({ id }: { id: keyof typeof itemRefs.current }) => {
itemRefs.current[id] = itemRefs.current[id] || createRef();
});
Object.keys(integrations ?? {}).forEach((k) => {
itemRefs.current[k] = itemRefs.current[k] || createRef();
});
}
// change column count depending on the width and the gridRef
useEffect(() => {
if (areaType === 'sidebar') return;
gridRef.current?.column(Math.floor(width / 64), 'moveScale');
}, [gridRef, width]);
const onChange = isEditMode
? (changedNode: GridStackNode) => {
if (!configName) return;
const itemType = changedNode.el?.getAttribute('data-type');
const itemId = changedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
// Updates the config and defines the new position of the item
updateConfig(configName, (previous) => {
const currentItem =
itemType === 'service'
? previous.services.find((x) => x.id === itemId)
: previous.integrations[itemId as keyof typeof previous.integrations];
if (!currentItem) return previous;
currentItem.shape = {
location: {
x: changedNode.x ?? currentItem.shape.location.x,
y: changedNode.y ?? currentItem.shape.location.y,
},
size: {
width: changedNode.w ?? currentItem.shape.size.width,
height: changedNode.h ?? currentItem.shape.size.height,
},
};
if (itemType === 'service') {
return {
...previous,
services: [
...previous.services.filter((x) => x.id !== itemId),
{ ...(currentItem as ServiceType) },
],
};
}
const integrationsCopy = { ...previous.integrations };
integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any;
return {
...previous,
integrations: integrationsCopy,
};
});
}
: () => {};
const onAdd = isEditMode
? (addedNode: GridStackNode) => {
if (!configName) return;
const itemType = addedNode.el?.getAttribute('data-type');
const itemId = addedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
// Updates the config and defines the new position and wrapper of the item
updateConfig(configName, (previous) => {
const currentItem =
itemType === 'service'
? previous.services.find((x) => x.id === itemId)
: previous.integrations[itemId as keyof typeof previous.integrations];
if (!currentItem) return previous;
if (areaType === 'sidebar') {
currentItem.area = {
type: areaType,
properties: {
location: areaId as 'right' | 'left',
},
};
} else {
currentItem.area = {
type: areaType,
properties: {
id: areaId,
},
};
}
currentItem.shape = {
location: {
x: addedNode.x ?? currentItem.shape.location.x,
y: addedNode.y ?? currentItem.shape.location.y,
},
size: {
width: addedNode.w ?? currentItem.shape.size.width,
height: addedNode.h ?? currentItem.shape.size.height,
},
};
if (itemType === 'service') {
return {
...previous,
services: [
...previous.services.filter((x) => x.id !== itemId),
{ ...(currentItem as ServiceType) },
],
};
}
const integrationsCopy = { ...previous.integrations };
integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any;
return {
...previous,
integrations: integrationsCopy,
};
});
}
: () => {};
// initialize the gridstack
useLayoutEffect(() => {
initializeGridstack(
areaType,
wrapperRef,
gridRef,
itemRefs,
areaId,
items,
integrations ?? {},
isEditMode,
{
onChange,
onAdd,
}
);
}, [items.length, wrapperRef.current, Object.keys(integrations ?? {}).length]);
return {
items,
integrations: integrations ?? {},
refs: {
items: itemRefs,
wrapper: wrapperRef,
gridstack: gridRef,
},
};
};