mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-21 23:12:16 +01:00
Rename sort -> type
This commit is contained in:
@@ -37,7 +37,7 @@ export const SearchCustomization = ({ allMediaIntegrations }: IntegrationCustomi
|
||||
data={allMediaIntegrations.map((x) => ({
|
||||
value: x.id,
|
||||
label: x.name,
|
||||
sort: x.sort,
|
||||
type: x.type,
|
||||
}))}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -45,16 +45,16 @@ export const SearchCustomization = ({ allMediaIntegrations }: IntegrationCustomi
|
||||
};
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
sort: IntegrationType;
|
||||
type: IntegrationType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const IntegrationSelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ label, sort, ...others }: ItemProps, ref) => {
|
||||
({ label, type, ...others }: ItemProps, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<Avatar size={20} src={integrationTypes[sort].iconUrl} />
|
||||
<Avatar size={20} src={integrationTypes[type].iconUrl} />
|
||||
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
@@ -89,7 +89,7 @@ const IntegrationSelectValue =
|
||||
})}
|
||||
>
|
||||
<Box mr={10}>
|
||||
<Avatar size="xs" src={integrationTypes[current!.sort].iconUrl} />
|
||||
<Avatar size="xs" src={integrationTypes[current!.type].iconUrl} />
|
||||
</Box>
|
||||
<Box sx={{ lineHeight: 1, fontSize: rem(12) }}>{label}</Box>
|
||||
<CloseButton
|
||||
|
||||
@@ -38,8 +38,8 @@ export const ChangeWidgetPositionModal = ({
|
||||
closeModal(id);
|
||||
};
|
||||
|
||||
const widthData = useWidthData(innerProps.widget.sort);
|
||||
const heightData = useHeightData(innerProps.widget.sort);
|
||||
const widthData = useWidthData(innerProps.widget.type);
|
||||
const heightData = useHeightData(innerProps.widget.type);
|
||||
|
||||
return (
|
||||
<CommonChangePositionModal
|
||||
|
||||
@@ -16,18 +16,18 @@ interface WidgetsMenuProps {
|
||||
}
|
||||
|
||||
export const WidgetsMenu = ({ widget }: WidgetsMenuProps) => {
|
||||
const { t } = useTranslation(`modules/${widget.sort}`);
|
||||
const { t } = useTranslation(`modules/${widget.type}`);
|
||||
const wrapperColumnCount = useWrapperColumnCount();
|
||||
const resizeGridItem = useResizeGridItem();
|
||||
const { removeItem } = useItemActions();
|
||||
|
||||
if (!widget || !wrapperColumnCount) return null;
|
||||
// Then get the widget definition
|
||||
const widgetDefinitionObject = WidgetsDefinitions[widget.sort as keyof typeof WidgetsDefinitions];
|
||||
const widgetDefinitionObject = WidgetsDefinitions[widget.type as keyof typeof WidgetsDefinitions];
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
openRemoveItemModal({
|
||||
name: widget.sort,
|
||||
name: widget.type,
|
||||
onConfirm() {
|
||||
removeItem({
|
||||
itemId: widget.id,
|
||||
@@ -55,7 +55,7 @@ export const WidgetsMenu = ({ widget }: WidgetsMenuProps) => {
|
||||
title: <Title order={4}>{t('descriptor.settings.title')}</Title>,
|
||||
innerProps: {
|
||||
widgetId: widget.id,
|
||||
widgetType: widget.sort,
|
||||
widgetType: widget.type,
|
||||
options: widget.options,
|
||||
widgetOptions: widgetDefinitionObject.options,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
import { widgetCreationSchema, widgetSortSchema } from '~/validations/widget';
|
||||
import { widgetCreationSchema, widgetTypeSchema } from '~/validations/widget';
|
||||
import { IWidgetDefinition } from '~/widgets/widgets';
|
||||
|
||||
import { useUpdateBoard } from '../../board-actions';
|
||||
@@ -13,7 +13,7 @@ type UpdateWidgetOptions = {
|
||||
};
|
||||
|
||||
type CreateWidget = {
|
||||
sort: z.infer<typeof widgetSortSchema>;
|
||||
type: z.infer<typeof widgetTypeSchema>;
|
||||
definition: IWidgetDefinition;
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useWidgetActions = () => {
|
||||
);
|
||||
|
||||
const createWidget = useCallback(
|
||||
({ sort, definition }: CreateWidget) => {
|
||||
({ type, definition }: CreateWidget) => {
|
||||
updateBoard((prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useWidgetActions = () => {
|
||||
const widget = {
|
||||
id: v4(),
|
||||
kind: 'widget',
|
||||
sort,
|
||||
type,
|
||||
options: Object.entries(definition.options).reduce(
|
||||
(prev, [k, v]) => {
|
||||
const newPrev = prev;
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SectionContent({ items, refs }: SectionContentProps) {
|
||||
</GridstackItemWrapper>
|
||||
))}
|
||||
{widgets.map((widget) => {
|
||||
const definition = Widgets[widget.sort];
|
||||
const definition = Widgets[widget.type];
|
||||
if (!definition) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AvailableIntegrationElements = ({
|
||||
|
||||
<Grid>
|
||||
{objectEntries(widgets).map(([k, v]) => (
|
||||
<WidgetElementType key={k} sort={k} image={v.icon} widget={v} modalId={modalId} />
|
||||
<WidgetElementType key={k} type={k} image={v.icon} widget={v} modalId={modalId} />
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { Icon, IconChecks } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { WidgetSort } from '~/server/db/items';
|
||||
import { WidgetType } from '~/server/db/items';
|
||||
import { IWidgetDefinition } from '~/widgets/widgets';
|
||||
|
||||
import { useWidgetActions } from '../../Items/Widget/widget-actions';
|
||||
import { GenericAvailableElementType } from '../Shared/GenericElementType';
|
||||
|
||||
interface WidgetElementTypeProps {
|
||||
sort: WidgetSort;
|
||||
type: WidgetType;
|
||||
image: string | Icon;
|
||||
disabled?: boolean;
|
||||
widget: IWidgetDefinition;
|
||||
@@ -17,19 +17,19 @@ interface WidgetElementTypeProps {
|
||||
}
|
||||
|
||||
export const WidgetElementType = ({
|
||||
sort,
|
||||
type,
|
||||
image,
|
||||
disabled,
|
||||
widget,
|
||||
modalId,
|
||||
}: WidgetElementTypeProps) => {
|
||||
const { closeModal } = useModals();
|
||||
const { t } = useTranslation(`modules/${sort}`);
|
||||
const { t } = useTranslation(`modules/${type}`);
|
||||
const { createWidget } = useWidgetActions();
|
||||
|
||||
const handleAddition = async () => {
|
||||
createWidget({
|
||||
sort,
|
||||
type,
|
||||
definition: widget,
|
||||
});
|
||||
closeModal(modalId);
|
||||
|
||||
@@ -49,7 +49,7 @@ export const Search = ({ isMobile, autoFocus }: SearchProps) => {
|
||||
)
|
||||
.filter(
|
||||
(engine) =>
|
||||
engine.sort !== 'movie' || board?.mediaIntegrations.some((x) => x.sort === engine.value)
|
||||
engine.sort !== 'movie' || board?.mediaIntegrations.some((x) => x.type === engine.value)
|
||||
)
|
||||
.map((engine) => ({
|
||||
...engine,
|
||||
|
||||
@@ -246,7 +246,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
{media.type === 'overseer' && !media.overseerrResult?.mediaInfo?.mediaAddedAt && (
|
||||
<>
|
||||
<RequestModal
|
||||
base={media.overseerrResult as Result}
|
||||
base={media.overseerrResult}
|
||||
opened={opened}
|
||||
setOpened={setOpened}
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const applyCreateWidgetChanges = (
|
||||
const widgetId = randomUUID();
|
||||
changes.widgets.create.push({
|
||||
id: widgetId,
|
||||
sort: widget.sort,
|
||||
type: widget.type,
|
||||
itemId: widget.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ export type MapWidget = Awaited<ReturnType<typeof getWidgetsForSectionsAsync>>[n
|
||||
export const mapWidget = (widgetItem: MapWidget) => {
|
||||
const { sectionId, itemId, id, ...commonLayoutItem } = widgetItem.item.layouts.at(0)!;
|
||||
const common = { ...commonLayoutItem, id: itemId };
|
||||
const { id: _id, itemId: _itemId, sort, item, options, integrations, ...widget } = widgetItem;
|
||||
const { id: _id, itemId: _itemId, type, item, options, integrations, ...widget } = widgetItem;
|
||||
return {
|
||||
...common,
|
||||
...widget,
|
||||
kind: 'widget' as const,
|
||||
sort,
|
||||
type,
|
||||
options: mapWidgetOptions(options),
|
||||
integrations: integrations.map((x) => ({
|
||||
...x.integration,
|
||||
|
||||
@@ -43,10 +43,10 @@ export const calendarRouter = createTRPCRouter({
|
||||
]);
|
||||
|
||||
const promises = widget.integrations.map(async (integration) => {
|
||||
const endpoint = integrationTypeEndpointMap.get(integration.sort);
|
||||
const endpoint = integrationTypeEndpointMap.get(integration.type);
|
||||
if (!endpoint) {
|
||||
return {
|
||||
sort: integration.sort,
|
||||
type: integration.type,
|
||||
items: [],
|
||||
success: false,
|
||||
};
|
||||
@@ -62,7 +62,7 @@ export const calendarRouter = createTRPCRouter({
|
||||
const end = new Date(input.year, input.month, 0); // Last day of month
|
||||
|
||||
const apiKey = integration.secrets.find((x) => x.key === 'apiKey')?.value;
|
||||
if (!apiKey) return { sort: integration.sort, items: [], success: false };
|
||||
if (!apiKey) return { type: integration.type, items: [], success: false };
|
||||
const url = new URL(`${origin}${endpoint}`);
|
||||
url.searchParams.set('apiKey', apiKey);
|
||||
url.searchParams.set('end', end.toISOString());
|
||||
@@ -74,13 +74,13 @@ export const calendarRouter = createTRPCRouter({
|
||||
|
||||
return axios
|
||||
.get(url.toString())
|
||||
.then((x) => ({ sort: integration.sort, items: x.data as unknown[], success: true }))
|
||||
.then((x) => ({ type: integration.type, items: x.data as unknown[], success: true }))
|
||||
.catch((err) => {
|
||||
Consola.error(
|
||||
`failed to process request for integration '${integration.sort}' (${integration.name}): ${err}`
|
||||
`failed to process request for integration '${integration.type}' (${integration.name}): ${err}`
|
||||
);
|
||||
return {
|
||||
sort: integration.sort,
|
||||
type: integration.type,
|
||||
items: [],
|
||||
success: false,
|
||||
};
|
||||
@@ -95,10 +95,10 @@ export const calendarRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
return {
|
||||
tvShows: medias.filter((m) => m.sort === 'sonarr').flatMap((m) => m.items),
|
||||
movies: medias.filter((m) => m.sort === 'radarr').flatMap((m) => m.items),
|
||||
books: medias.filter((m) => m.sort === 'readarr').flatMap((m) => m.items),
|
||||
musics: medias.filter((m) => m.sort === 'lidarr').flatMap((m) => m.items),
|
||||
tvShows: medias.filter((m) => m.type === 'sonarr').flatMap((m) => m.items),
|
||||
movies: medias.filter((m) => m.type === 'radarr').flatMap((m) => m.items),
|
||||
books: medias.filter((m) => m.type === 'readarr').flatMap((m) => m.items),
|
||||
musics: medias.filter((m) => m.type === 'lidarr').flatMap((m) => m.items),
|
||||
totalCount: medias.reduce((p, c) => p + c.items.length, 0),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -127,26 +127,6 @@ export const configRouter = createTRPCRouter({
|
||||
|
||||
newConfig = {
|
||||
...newConfig,
|
||||
widgets: [
|
||||
...newConfig.widgets.map((x) => {
|
||||
if (x.type !== 'rss') {
|
||||
return x;
|
||||
}
|
||||
|
||||
const rssWidget = x as IRssWidget;
|
||||
|
||||
return {
|
||||
...rssWidget,
|
||||
properties: {
|
||||
...rssWidget.properties,
|
||||
rssFeedUrl:
|
||||
typeof rssWidget.properties.rssFeedUrl === 'string'
|
||||
? [rssWidget.properties.rssFeedUrl]
|
||||
: rssWidget.properties.rssFeedUrl,
|
||||
},
|
||||
} as IRssWidget;
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// Save the body in the /data/config folder with the slug as filename
|
||||
|
||||
@@ -25,7 +25,7 @@ export const downloadRouter = createTRPCRouter({
|
||||
z.object({
|
||||
boardId: z.string(),
|
||||
widgetId: z.string(),
|
||||
sort: z.enum(['torrents-status', 'dlspeed']),
|
||||
type: z.enum(['torrents-status', 'dlspeed']),
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
@@ -33,7 +33,7 @@ export const downloadRouter = createTRPCRouter({
|
||||
input.boardId,
|
||||
input.widgetId,
|
||||
ctx.session?.user,
|
||||
input.sort
|
||||
input.type
|
||||
);
|
||||
|
||||
if (!widget) {
|
||||
@@ -107,7 +107,7 @@ const GetDataFromClient = async (
|
||||
.reduce((acc, torrent) => acc + torrent, 0),
|
||||
});
|
||||
|
||||
switch (integration.sort) {
|
||||
switch (integration.type) {
|
||||
case 'deluge': {
|
||||
return reduceTorrent(
|
||||
await new Deluge({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { adminProcedure, createTRPCRouter } from '../trpc';
|
||||
export const integrationRouter = createTRPCRouter({
|
||||
allMedia: adminProcedure.query(async () => {
|
||||
return await db.query.integrations.findMany({
|
||||
where: inArray(integrations.sort, ['jellyseerr', 'overseerr']),
|
||||
where: inArray(integrations.type, ['jellyseerr', 'overseerr']),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export const overseerrRouter = createTRPCRouter({
|
||||
.where(
|
||||
and(
|
||||
eq(boardIntegrations.boardId, input.boardId),
|
||||
eq(integrations.sort, input.integration)
|
||||
eq(integrations.type, input.integration)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface UsenetClient {
|
||||
}
|
||||
|
||||
export function createUsenetClient(integration: WidgetIntegration) {
|
||||
if (integration.sort === 'nzbGet') return new NzbgetUsenetClient(integration);
|
||||
if (integration.type === 'nzbGet') return new NzbgetUsenetClient(integration);
|
||||
return new SabnzbdUsenetClient(integration);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +1,37 @@
|
||||
import { IconKey, IconPassword, IconUser, TablerIconsProps } from '@tabler/icons-react';
|
||||
import widgets from '~/widgets';
|
||||
|
||||
import { objectEntries, objectKeys } from '../../tools/object';
|
||||
import { objectKeys } from '../../tools/object';
|
||||
|
||||
type IntegrationTypeDefinition = {
|
||||
export type IntegrationTypeDefinition = {
|
||||
secrets: IntegrationSecretKey[];
|
||||
iconUrl: string;
|
||||
label: string;
|
||||
groups: IntegrationGroup[];
|
||||
testEndpoint?: string;
|
||||
};
|
||||
type IntegrationGroup =
|
||||
| 'mediaServer'
|
||||
| 'mediaRequest'
|
||||
| 'mediaApp'
|
||||
| 'usenet'
|
||||
| 'torrent'
|
||||
| 'download'
|
||||
| 'dns';
|
||||
|
||||
type IntegrationSecretDefinition = {
|
||||
visibility: IntegrationSecretVisibility;
|
||||
icon: (props: TablerIconsProps) => JSX.Element;
|
||||
};
|
||||
|
||||
export const colorSchemes = ['environment', 'light', 'dark'] as const;
|
||||
export const firstDaysOfWeek = ['monday', 'saturday', 'sunday'] as const;
|
||||
export const integrationSecretVisibility = ['private', 'public'] as const;
|
||||
export const integrationSecrets = {
|
||||
apiKey: {
|
||||
visibility: 'private',
|
||||
icon: IconKey,
|
||||
},
|
||||
username: {
|
||||
visibility: 'public',
|
||||
icon: IconUser,
|
||||
},
|
||||
password: {
|
||||
visibility: 'private',
|
||||
icon: IconPassword,
|
||||
},
|
||||
} satisfies Record<string, IntegrationSecretDefinition>;
|
||||
export const widgetSorts = objectKeys(widgets);
|
||||
export const widgetOptionTypes = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'object',
|
||||
'array',
|
||||
'null',
|
||||
] as const;
|
||||
|
||||
export const widgetTypes = objectKeys(widgets);
|
||||
export const widgetOptionTypes = ['string', 'number', 'boolean', 'object', 'array', 'null'] as const;
|
||||
export const boardBackgroundImageAttachmentTypes = ['fixed', 'scroll'] as const;
|
||||
export const boardBackgroundImageRepeatTypes = [
|
||||
'repeat',
|
||||
'repeat-x',
|
||||
'repeat-y',
|
||||
'no-repeat',
|
||||
] as const;
|
||||
export const boardBackgroundImageRepeatTypes = ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'] as const;
|
||||
export const boardBackgroundImageSizeTypes = ['cover', 'contain'] as const;
|
||||
export const appNamePositions = ['right', 'left', 'top', 'bottom'] as const;
|
||||
export const appNameStyles = ['normal', 'hide', 'hover'] as const;
|
||||
@@ -66,107 +42,95 @@ export const statusCodeTypes = [
|
||||
'clientError',
|
||||
'serverError',
|
||||
] as const;
|
||||
export const sectionTypes = ['sidebar', 'empty', 'category', 'hidden'] as const;
|
||||
export const layoutKinds = ['mobile', 'desktop'] as const;
|
||||
const sectionTypes = ['sidebar', 'empty', 'category', 'hidden'] as const;
|
||||
const layoutKinds = ['mobile', 'desktop'] as const;
|
||||
export const integrationTypes = {
|
||||
readarr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
||||
label: 'Readarr',
|
||||
groups: ['mediaApp'],
|
||||
testEndpoint: '/api',
|
||||
},
|
||||
radarr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/radarr.png',
|
||||
label: 'Radarr',
|
||||
groups: ['mediaApp'],
|
||||
testEndpoint: '/api',
|
||||
},
|
||||
sonarr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sonarr.png',
|
||||
label: 'Sonarr',
|
||||
groups: ['mediaApp'],
|
||||
testEndpoint: '/api',
|
||||
},
|
||||
lidarr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/lidarr.png',
|
||||
label: 'Lidarr',
|
||||
groups: ['mediaApp'],
|
||||
testEndpoint: '/api',
|
||||
},
|
||||
sabnzbd: {
|
||||
secrets: [],
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sabnzbd.png',
|
||||
label: 'SABnzbd',
|
||||
groups: ['usenet', 'download'],
|
||||
},
|
||||
jellyseerr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png',
|
||||
label: 'Jellyseerr',
|
||||
groups: ['mediaRequest'],
|
||||
},
|
||||
overseerr: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/overseerr.png',
|
||||
label: 'Overseerr',
|
||||
groups: ['mediaRequest'],
|
||||
},
|
||||
deluge: {
|
||||
secrets: ['password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/deluge.png',
|
||||
label: 'Deluge',
|
||||
groups: ['torrent'],
|
||||
},
|
||||
qBittorrent: {
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/qbittorrent.png',
|
||||
label: 'qBittorrent',
|
||||
groups: ['torrent'],
|
||||
},
|
||||
transmission: {
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/transmission.png',
|
||||
label: 'Transmission',
|
||||
groups: ['torrent'],
|
||||
},
|
||||
plex: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/plex.png',
|
||||
label: 'Plex',
|
||||
groups: ['mediaServer'],
|
||||
},
|
||||
jellyfin: {
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
||||
label: 'Jellyfin',
|
||||
groups: ['mediaServer'],
|
||||
},
|
||||
nzbGet: {
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/nzbget.png',
|
||||
label: 'NZBGet',
|
||||
groups: ['usenet', 'download'],
|
||||
},
|
||||
pihole: {
|
||||
secrets: ['apiKey'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pi-hole.png',
|
||||
label: 'PiHole',
|
||||
groups: ['dns'],
|
||||
},
|
||||
adGuardHome: {
|
||||
secrets: ['username', 'password'],
|
||||
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/adguard-home.png',
|
||||
label: 'AdGuard Home',
|
||||
groups: ['dns'],
|
||||
},
|
||||
} satisfies Record<string, IntegrationTypeDefinition>;
|
||||
|
||||
export type ColorScheme = (typeof colorSchemes)[number];
|
||||
export type FirstDayOfWeek = (typeof firstDaysOfWeek)[number];
|
||||
export type IntegrationType = keyof typeof integrationTypes;
|
||||
export type IntegrationSecretVisibility = (typeof integrationSecretVisibility)[number];
|
||||
export type IntegrationSecretKey = keyof typeof integrationSecrets;
|
||||
export type WidgetSort = (typeof widgetSorts)[number];
|
||||
export type WidgetType = (typeof widgetTypes)[number];
|
||||
export type WidgetOptionType = (typeof widgetOptionTypes)[number];
|
||||
export type BoardBackgroundImageAttachmentType =
|
||||
(typeof boardBackgroundImageAttachmentTypes)[number];
|
||||
@@ -177,17 +141,3 @@ export type AppNameStyle = (typeof appNameStyles)[number];
|
||||
export type StatusCodeType = (typeof statusCodeTypes)[number];
|
||||
export type SectionType = (typeof sectionTypes)[number];
|
||||
export type LayoutKind = (typeof layoutKinds)[number];
|
||||
|
||||
type InferIntegrationTypeFromGroup<TGroup extends IntegrationGroup> = {
|
||||
[key in keyof typeof integrationTypes]: (typeof integrationTypes)[key] extends {
|
||||
groups: TGroup[];
|
||||
}
|
||||
? key
|
||||
: never;
|
||||
}[keyof typeof integrationTypes];
|
||||
|
||||
export const integrationGroup = <TGroup extends IntegrationGroup>(group: TGroup) => {
|
||||
return objectEntries(integrationTypes)
|
||||
.filter(([, { groups }]) => groups.some((g) => group === g))
|
||||
.map(([key]) => key) as InferIntegrationTypeFromGroup<TGroup>[];
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getIntegrations<TIntegrations extends IntegrationType>(
|
||||
|
||||
export const getIntegrationsForWidget = async <TIntegrations extends IntegrationType>(
|
||||
boardId: string,
|
||||
sorts: TIntegrations[],
|
||||
types: TIntegrations[],
|
||||
|
||||
user: User | null | undefined,
|
||||
widgetId: 'ignore' | (string & {})
|
||||
@@ -24,7 +24,7 @@ export const getIntegrationsForWidget = async <TIntegrations extends Integration
|
||||
where: and(
|
||||
eq(items.boardId, boardId),
|
||||
user ? undefined : eq(boards.allowGuests, true),
|
||||
sorts.length >= 1 ? inArray(integrations.sort, sorts) : undefined,
|
||||
types.length >= 1 ? inArray(integrations.type, types) : undefined,
|
||||
widgetId !== 'ignore' ? eq(items.id, widgetId) : undefined
|
||||
),
|
||||
with: {
|
||||
@@ -46,7 +46,7 @@ export const getIntegrationsForWidget = async <TIntegrations extends Integration
|
||||
});
|
||||
return widgetItems
|
||||
.flatMap((x) => x.widget?.integrations ?? [])
|
||||
.map((x) => ({ ...x.integration, sort: x.integration.sort as TIntegrations }));
|
||||
.map((x) => ({ ...x.integration, type: x.integration.type as TIntegrations }));
|
||||
};
|
||||
|
||||
export async function getIntegrationAsync(integrationId: string) {
|
||||
|
||||
@@ -8,18 +8,18 @@ import { InferWidgetOptions } from '~/widgets/widgets';
|
||||
import { db } from '..';
|
||||
import { boards, items, widgets } from '../schema';
|
||||
|
||||
export const getWidgetAsync = async <TSort extends keyof typeof widgetDefinitions>(
|
||||
export const getWidgetAsync = async <TType extends keyof typeof widgetDefinitions>(
|
||||
boardId: string,
|
||||
id: string,
|
||||
user: User | null | undefined,
|
||||
sort: TSort
|
||||
type: TType
|
||||
) => {
|
||||
const widgetItem = await db.query.items.findFirst({
|
||||
where: and(
|
||||
eq(items.boardId, boardId),
|
||||
eq(items.id, id),
|
||||
user ? undefined : eq(boards.allowGuests, true),
|
||||
eq(widgets.sort, sort)
|
||||
eq(widgets.type, type)
|
||||
),
|
||||
with: {
|
||||
widget: {
|
||||
@@ -44,21 +44,21 @@ export const getWidgetAsync = async <TSort extends keyof typeof widgetDefinition
|
||||
},
|
||||
});
|
||||
|
||||
if (!widgetItem) {
|
||||
if (!widgetItem || !widgetItem.widget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mappedOptions = mapWidgetOptions(
|
||||
widgetItem.widget!.options.sort((a, b) => a.path.localeCompare(b.path))
|
||||
widgetItem.widget.options.sort((a, b) => a.path.localeCompare(b.path))
|
||||
);
|
||||
objectEntries(widgetDefinitions[sort].options).forEach(([key, definition]) => {
|
||||
objectEntries(widgetDefinitions[type].options).forEach(([key, definition]) => {
|
||||
mappedOptions[key] = mappedOptions[key] ?? definition.defaultValue;
|
||||
});
|
||||
|
||||
return {
|
||||
id: widgetItem.id,
|
||||
sort: widgetItem.widget!.sort,
|
||||
options: mappedOptions as InferWidgetOptions<(typeof widgetDefinitions)[TSort]>,
|
||||
type: widgetItem.widget.type,
|
||||
options: mappedOptions as InferWidgetOptions<(typeof widgetDefinitions)[TType]>,
|
||||
integrations: widgetItem.widget!.integrations.map((i) => ({
|
||||
...i.integration,
|
||||
})),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MantineColor } from '@mantine/core';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { relations, type InferSelectModel } from 'drizzle-orm';
|
||||
import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { type AdapterAccount } from 'next-auth/adapters';
|
||||
|
||||
@@ -12,12 +12,11 @@ import type {
|
||||
ColorScheme,
|
||||
FirstDayOfWeek,
|
||||
IntegrationSecretKey,
|
||||
IntegrationSecretVisibility,
|
||||
IntegrationType,
|
||||
LayoutKind,
|
||||
SectionType,
|
||||
WidgetOptionType,
|
||||
WidgetSort,
|
||||
WidgetType,
|
||||
} from './items';
|
||||
|
||||
export const users = sqliteTable('user', {
|
||||
@@ -156,7 +155,7 @@ export const boards = sqliteTable('board', {
|
||||
|
||||
export const integrations = sqliteTable('integration', {
|
||||
id: text('id').notNull().primaryKey(),
|
||||
sort: text('sort').$type<IntegrationType>().notNull(),
|
||||
type: text('type').$type<IntegrationType>().notNull(),
|
||||
name: text('name').notNull(),
|
||||
url: text('url').notNull(),
|
||||
});
|
||||
@@ -166,7 +165,6 @@ export const integrationSecrets = sqliteTable(
|
||||
{
|
||||
key: text('key').$type<IntegrationSecretKey>().notNull(),
|
||||
value: text('value'),
|
||||
visibility: text('visibility').$type<IntegrationSecretVisibility>().notNull(),
|
||||
integrationId: text('integration_id')
|
||||
.notNull()
|
||||
.references(() => integrations.id, { onDelete: 'cascade' }),
|
||||
@@ -189,7 +187,7 @@ export const boardIntegrations = sqliteTable(
|
||||
|
||||
export const widgets = sqliteTable('widget', {
|
||||
id: text('id').notNull().primaryKey(),
|
||||
sort: text('sort').$type<WidgetSort>().notNull(),
|
||||
type: text('sort').$type<WidgetType>().notNull(),
|
||||
itemId: text('item_id')
|
||||
.notNull()
|
||||
.references(() => items.id, { onDelete: 'cascade' }),
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { widgetSorts } from '~/server/db/items';
|
||||
import { widgetTypes } from '~/server/db/items';
|
||||
|
||||
import { appSchema } from './app';
|
||||
import { commonItemSchema } from './item';
|
||||
|
||||
export const widgetSortSchema = z.enum([widgetSorts[0], ...widgetSorts.slice(1)]);
|
||||
export const widgetTypeSchema = z.enum([widgetTypes[0], ...widgetTypes.slice(1)]);
|
||||
|
||||
export const widgetCreationSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('widget'),
|
||||
sort: widgetSortSchema,
|
||||
type: widgetTypeSchema,
|
||||
options: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface WidgetWrapperProps {
|
||||
|
||||
// If a property has no value, set it to the default value
|
||||
const useWidgetWithDefaultOptionValues = <T extends WidgetItem>(widget: T): T => {
|
||||
const definition = Widgets[widget.sort];
|
||||
const definition = Widgets[widget.type];
|
||||
|
||||
const newProps: Record<string, unknown> = {};
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
NormalizedDownloadQueueResponse,
|
||||
TorrentTotalDownload,
|
||||
} from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
import definition, { ITorrentNetworkTraffic } from './TorrentNetworkTrafficTile';
|
||||
import { useGetDownloadClientsQueue } from './useGetNetworkSpeed';
|
||||
|
||||
interface TorrentNetworkTrafficTileProps {
|
||||
widget: ITorrentNetworkTraffic;
|
||||
@@ -42,11 +42,16 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
const [clientDataHistory, setClientDataHistory] = useListState<NormalizedDownloadQueueResponse>();
|
||||
|
||||
const { data, dataUpdatedAt } = useGetDownloadClientsQueue({
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
sort: 'dlspeed',
|
||||
});
|
||||
const { data, dataUpdatedAt } = api.download.get.useQuery(
|
||||
{
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
type: definition.id,
|
||||
},
|
||||
{
|
||||
refetchInterval: 3000,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -169,7 +174,7 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
return (
|
||||
<Group key={`download-client-tooltip-${index}`}>
|
||||
<AppAvatar iconUrl={integrationTypes[current.sort].iconUrl} />
|
||||
<AppAvatar iconUrl={integrationTypes[current.type].iconUrl} />
|
||||
|
||||
<Stack spacing={0}>
|
||||
<Text size="sm">{current.name}</Text>
|
||||
@@ -257,7 +262,7 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
withArrow
|
||||
withinPortal
|
||||
>
|
||||
<AppAvatar iconUrl={integrationTypes[current.sort].iconUrl} />
|
||||
<AppAvatar iconUrl={integrationTypes[current.type].iconUrl} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RouterInputs, api } from '~/utils/api';
|
||||
|
||||
export const useGetDownloadClientsQueue = ({
|
||||
boardId,
|
||||
widgetId,
|
||||
sort,
|
||||
}: RouterInputs['download']['get']) => {
|
||||
return api.download.get.useQuery(
|
||||
{
|
||||
boardId,
|
||||
widgetId,
|
||||
sort,
|
||||
},
|
||||
{
|
||||
refetchInterval: 3000,
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -22,8 +22,8 @@ import { useCardStyles } from '~/components/layout/Common/useCardStyles';
|
||||
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
||||
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
import { AppIntegrationType } from '~/types/app';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
|
||||
import { defineWidget } from '../helper';
|
||||
import { InferWidget } from '../widgets';
|
||||
import { BitTorrentQueueItem } from './TorrentQueueItem';
|
||||
@@ -99,11 +99,16 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
isError: boolean;
|
||||
isInitialLoading: boolean;
|
||||
dataUpdatedAt: number;
|
||||
} = useGetDownloadClientsQueue({
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
sort: 'torrents-status',
|
||||
});
|
||||
} = api.download.get.useQuery(
|
||||
{
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
type: definition.id,
|
||||
},
|
||||
{
|
||||
refetchInterval: 3000,
|
||||
}
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user