WIP on integrations panel

This commit is contained in:
Thomas Camlong
2023-12-12 17:20:55 +01:00
parent 1c756b5ada
commit 8f50795db7
14 changed files with 1616 additions and 155 deletions

View File

@@ -11,7 +11,7 @@
"layout.manage.navigation.**",
],
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.patterns": {

View File

@@ -0,0 +1,134 @@
CREATE TABLE `app_status_code` (
`app_id` text NOT NULL,
`code` integer NOT NULL,
PRIMARY KEY(`app_id`, `code`),
FOREIGN KEY (`app_id`) REFERENCES `app`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`code`) REFERENCES `status_code`(`code`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `app` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`description` text,
`internal_url` text NOT NULL,
`external_url` text,
`icon_url` text NOT NULL,
`open_in_new_tab` integer DEFAULT false NOT NULL,
`is_ping_enabled` integer DEFAULT false NOT NULL,
`font_size` integer DEFAULT 16 NOT NULL,
`name_position` text DEFAULT 'top' NOT NULL,
`name_style` text DEFAULT 'normal' NOT NULL,
`name_line_clamp` integer DEFAULT 1 NOT NULL,
`item_id` text NOT NULL,
FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `board_integration` (
`board_id` text NOT NULL,
`integration_id` text NOT NULL,
PRIMARY KEY(`board_id`, `integration_id`)
);
--> statement-breakpoint
CREATE TABLE `board` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`is_ping_enabled` integer DEFAULT false NOT NULL,
`allow_guests` integer DEFAULT false NOT NULL,
`page_title` text,
`meta_title` text,
`logo_image_url` text,
`favicon_image_url` text,
`background_image_url` text,
`background_image_attachment` text DEFAULT 'fixed' NOT NULL,
`background_image_repeat` text DEFAULT 'no-repeat' NOT NULL,
`background_image_size` text DEFAULT 'cover' NOT NULL,
`primary_color` text DEFAULT 'red' NOT NULL,
`secondary_color` text DEFAULT 'orange' NOT NULL,
`primary_shade` integer DEFAULT 6 NOT NULL,
`app_opacity` integer DEFAULT 100 NOT NULL,
`custom_css` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `integration_secret` (
`key` text NOT NULL,
`value` text,
`integration_id` text NOT NULL,
PRIMARY KEY(`integration_id`, `key`),
FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `integration` (
`id` text PRIMARY KEY NOT NULL,
`type` text NOT NULL,
`name` text NOT NULL,
`url` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `item` (
`id` text PRIMARY KEY NOT NULL,
`kind` text NOT NULL,
`board_id` text NOT NULL,
FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `layout_item` (
`id` text PRIMARY KEY NOT NULL,
`section_id` text NOT NULL,
`item_id` text NOT NULL,
`x` integer NOT NULL,
`y` integer NOT NULL,
`width` integer NOT NULL,
`height` integer NOT NULL,
FOREIGN KEY (`section_id`) REFERENCES `section`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `layout` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`kind` text NOT NULL,
`show_right_sidebar` integer DEFAULT false NOT NULL,
`show_left_sidebar` integer DEFAULT false NOT NULL,
`column_count` integer DEFAULT 10 NOT NULL,
`board_id` text NOT NULL,
FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `section` (
`id` text PRIMARY KEY NOT NULL,
`kind` text NOT NULL,
`position` integer,
`name` text,
`layout_id` text NOT NULL,
FOREIGN KEY (`layout_id`) REFERENCES `layout`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `status_code` (
`code` integer PRIMARY KEY NOT NULL
);
--> statement-breakpoint
CREATE TABLE `widget_integration` (
`widget_id` text NOT NULL,
`integration_id` text NOT NULL,
PRIMARY KEY(`integration_id`, `widget_id`),
FOREIGN KEY (`widget_id`) REFERENCES `widget`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `widget_option` (
`path` text NOT NULL,
`value` text,
`type` text NOT NULL,
`widget_id` text NOT NULL,
PRIMARY KEY(`path`, `widget_id`),
FOREIGN KEY (`widget_id`) REFERENCES `widget`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `widget` (
`id` text PRIMARY KEY NOT NULL,
`sort` text NOT NULL,
`item_id` text NOT NULL,
FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade
);

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1695874816934,
"tag": "0000_supreme_the_captain",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1701527428740,
"tag": "0001_volatile_the_enforcers",
"breakpoints": true
}
]
}

View File

@@ -24,7 +24,8 @@
"test:coverage": "SKIP_ENV_VALIDATION=1 vitest run --coverage",
"docker:build": "turbo build && docker build . -t homarr:local-dev",
"docker:start": "docker run -p 7575:7575 --name homarr-development homarr:local-dev",
"db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle"
"db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle",
"db:create": "dotenv drizzle-kit push:sqlite"
},
"dependencies": {
"@auth/drizzle-adapter": "^0.3.2",
@@ -92,6 +93,7 @@
"html-entities": "^2.3.3",
"i18next": "^22.5.1",
"immer": "^10.0.2",
"nanoid": "^5.0.4",
"next": "13.4.12",
"next-auth": "^4.23.0",
"next-i18next": "^14.0.0",
@@ -234,4 +236,4 @@
]
}
}
}
}

View File

@@ -22,6 +22,9 @@
"contribute": "Contribute"
}
},
"integrations": {
"title": "Integrations"
},
"tools": {
"title": "Tools",
"items": {

View File

@@ -8,10 +8,8 @@ import {
Indicator,
NavLink,
Navbar,
Paper,
Text,
ThemeIcon,
useMantineTheme,
ThemeIcon
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
@@ -21,7 +19,6 @@ import {
IconBrandGithub,
IconGitFork,
IconHome,
IconInfoCircle,
IconInfoSmall,
IconLayoutDashboard,
IconMailForward,
@@ -29,7 +26,7 @@ import {
IconTool,
IconUser,
IconUsers,
TablerIconsProps,
TablerIconsProps
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';
@@ -42,6 +39,7 @@ import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
import { ConditionalWrapper } from '~/utils/security';
import { IconPlugConnected } from '@tabler/icons-react';
import { REPO_URL } from '../../../../data/constants';
import { type navigation } from '../../../../public/locales/en/layout/manage.json';
import { MainHeader } from '../header/Header';
@@ -96,6 +94,11 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
},
},
},
integrations: {
icon: IconPlugConnected,
onlyAdmin: true,
href: '/manage/integrations',
},
tools: {
icon: IconTool,
onlyAdmin: true,

View File

@@ -1,98 +1,42 @@
import { Button, Group, Stack, TextInput, useMantineTheme } from '@mantine/core';
import { UseFormReturnType, useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { getQueryKey } from '@trpc/react-query';
import { getCookie } from 'cookies-next';
import { produce } from 'immer';
import { useForm } from '@mantine/form';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { IntegrationTab } from '~/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/IntegrationTab';
import { AppType } from '~/types/app';
import { IntegrationTypeMap } from '~/types/config';
import { api } from '~/utils/api';
import { IntegrationType } from '~/server/db/items';
import { AppIntegrationType } from '~/types/app';
const defaultAppValues: AppType = {
id: uuidv4(),
name: 'Your app',
url: 'https://homarr.dev',
appearance: {
iconUrl: '/imgs/logo/logo.png',
appNameStatus: 'normal',
positionAppName: '-moz-initial',
lineClampAppName: 2
},
network: {
enabledStatusChecker: true,
statusCodes: ['200', '301', '302', '304', '307', '308'],
okStatus: [200, 301, 302, 304, 307, 308],
},
behaviour: {
isOpeningNewTab: true,
externalUrl: '',
},
area: {
type: 'wrapper',
properties: {
id: 'default',
},
},
shape: {},
integration: {
id: uuidv4(),
url: '',
type: undefined,
properties: [],
name: 'New integration',
},
};
export function AddIntegrationPanel({
globalForm,
queryKey,
integrations,
setIntegrations,
}: {
globalForm: UseFormReturnType<any>;
queryKey: QueryKey;
integrations: IntegrationTypeMap | undefined;
setIntegrations: React.Dispatch<React.SetStateAction<IntegrationTypeMap | undefined>>;
}) {
export function AddIntegrationPanel({}) {
const { t } = useTranslation(['board/integrations', 'common']);
const queryClient = useQueryClient();
const { primaryColor } = useMantineTheme();
const form = useForm<AppType>({
initialValues: defaultAppValues,
const form = useForm<IntegrationType>({
initialValues: 'jellyfin',
});
if (!integrations) {
return null;
}
return (
<form
onSubmit={form.onSubmit(({ integration }) => {
if (!integration.type || !integrations) return null;
const newIntegrations = produce(integrations, (draft) => {
integration.id = uuidv4();
// console.log(integration.type);
if (!integration.type) return;
// If integration type is not in integrations, add it
if (!draft[integration.type]) {
draft[integration.type] = [];
}
draft[integration.type].push(integration);
});
// queryClient.setQueryData(queryKey, newIntegrations);
form.reset();
setIntegrations(newIntegrations);
notifications.show({
title: t('integration.Added'),
message: t('integration.AddedDescription', { name: integration.name }),
color: 'green',
});
onSubmit={form.onSubmit((input) => {
console.log(input);
// if (!integration.type || !integrations) return null;
// const newIntegrations = produce(integrations, (draft) => {
// integration.id = uuidv4();
// // console.log(integration.type);
// if (!integration.type) return;
// // If integration type is not in integrations, add it
// if (!draft[integration.type]) {
// draft[integration.type] = [];
// }
// draft[integration.type].push(integration);
// });
// // queryClient.setQueryData(queryKey, newIntegrations);
// form.reset();
// setIntegrations(newIntegrations);
// notifications.show({
// title: t('integration.Added'),
// message: t('integration.AddedDescription', { name: integration.name }),
// color: 'green',
// });
})}
>
<Stack>

View File

@@ -1,64 +1,20 @@
import {
ActionIcon,
Autocomplete,
Avatar,
Badge,
Box,
Button,
Flex,
Group,
Pagination,
Table,
Text,
Title,
Tooltip,
useMantineTheme,
} from '@mantine/core';
import { Text, Title } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconPlus, IconTrash, IconUserDown, IconUserUp } from '@tabler/icons-react';
import { GetServerSideProps } from 'next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Head from 'next/head';
import Link from 'next/link';
import { useState } from 'react';
import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal';
import { openDeleteUserModal } from '~/components/Manage/User/delete-user.modal';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api';
import { AddIntegrationPanel } from './AddIntegrationPanel';
import { useQueryClient } from '@tanstack/react-query';
import { IntegrationTypeMap } from '~/types/config';
import { useConfigContext } from '~/config/provider';
import { useForm } from '@mantine/form';
const ManageUsersPage = () => {
const [activePage, setActivePage] = useState(0);
const [nonDebouncedSearch, setNonDebouncedSearch] = useState<string | undefined>('');
const [debouncedSearch] = useDebouncedValue<string | undefined>(nonDebouncedSearch, 200);
const { t } = useTranslation('manage/integrations');
const queryClient = useQueryClient();
const { primaryColor } = useMantineTheme();
const integrationsQuery: IntegrationTypeMap | undefined = queryClient.getQueryData(queryKey);
const mutation = api.config.save.useMutation();
const { config, name } = useConfigContext();
const [isLoading, setIsLoading] = useState(false);
const { data: sessionData } = useSession();
const [integrations, setIntegrations] = useState<IntegrationTypeMap | undefined>(
integrationsQuery
);
if (!integrations) {
return null;
}
const form = useForm({
initialValues: integrationsQuery,
});
const metaTitle = `${t('metaTitle')} • Homarr`;
@@ -70,12 +26,6 @@ const ManageUsersPage = () => {
<Title mb="md">{t('pageTitle')}</Title>
<Text mb="xl">{t('text')}</Text>
<AddIntegrationPanel
globalForm={form}
queryKey={queryKey}
integrations={integrations}
setIntegrations={setIntegrations}
/>
</ManageLayout>
);
};
@@ -85,15 +35,20 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
if (!session?.user.isAdmin) {
return {
notFound: true,
redirect: {
destination: '/401',
permanent: false,
},
};
}
const caller = integrationsRouter
const translations = await getServerSideTranslations(
manageNamespaces,
[...manageNamespaces, 'manage/integrations'],
ctx.locale,
undefined,
undefined
ctx.req,
ctx.res,
);
return {
props: {

View File

@@ -9,7 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download';
import { iconRouter } from './routers/icon';
import { integrationRouter } from './routers/integration';
import { integrationsRouter } from './routers/integration';
import { inviteRouter } from './routers/invite/invite-router';
import { layoutsRouter } from './routers/layout/layout.router';
import { mediaRequestsRouter } from './routers/media-request';
@@ -50,7 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
layouts: layoutsRouter,
integration: integrationRouter,
integration: integrationsRouter,
});
// export type definition of API

View File

@@ -1,13 +1,59 @@
import { inArray } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { db } from '~/server/db';
import { integrations } from '~/server/db/schema';
import { integrationSecrets, integrations } from '~/server/db/schema';
import { adminProcedure, createTRPCRouter } from '../trpc';
export const integrationRouter = createTRPCRouter({
export const integrationsRouter = createTRPCRouter({
allMedia: adminProcedure.query(async () => {
return await db.query.integrations.findMany({
where: inArray(integrations.type, ['jellyseerr', 'overseerr']),
});
}),
findAll: adminProcedure.query(async () => {
return await db.query.integrations.findMany({
with: {
secrets: true,
},
});
}),
addOne: adminProcedure
.input(
z.object({
name: z.string(),
type: z.any(),
url: z.string().url(),
secrets: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
})
)
.mutation(async ({ input }) => {
const newId = nanoid(8);
const integration = await db
.insert(integrations)
.values({
id: newId,
name: input.name,
type: input.type,
url: input.url,
})
.execute();
const secrets = await db.insert(integrationSecrets).values(
{
integrationId: newId,
key: 'apiKey',
value: 'test'
}
);
return { integration, secrets };
}),
});

View File

@@ -1,5 +1,5 @@
import type { MantineColor } from '@mantine/core';
import { relations, type InferSelectModel } from 'drizzle-orm';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { type AdapterAccount } from 'next-auth/adapters';

View File

@@ -58,7 +58,7 @@ export type IntegrationType =
| 'adGuardHome';
export type AppIntegrationType = {
type: IntegrationType | null;
type: IntegrationType;
properties: AppIntegrationPropertyType[];
};
@@ -69,8 +69,7 @@ export type ConfigAppIntegrationType = Omit<AppIntegrationType, 'properties'> &
export type AppIntegrationPropertyType = {
type: AppIntegrationPropertyAccessabilityType;
field: IntegrationField;
value?: string | null;
isDefined: boolean;
value: string;
};
export type AppIntegrationPropertyAccessabilityType = 'private' | 'public';

View File

@@ -7192,6 +7192,7 @@ __metadata:
html-entities: ^2.3.3
i18next: ^22.5.1
immer: ^10.0.2
nanoid: ^5.0.4
next: 13.4.12
next-auth: ^4.23.0
next-i18next: ^14.0.0
@@ -8929,6 +8930,15 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^5.0.4":
version: 5.0.4
resolution: "nanoid@npm:5.0.4"
bin:
nanoid: bin/nanoid.js
checksum: cf09cca3774f3147100948f7478f75f4c9ee97a4af65c328dd9abbd83b12f8bb35cf9f89a21c330f3b759d667a4cd0140ed84aa5fdd522c61e0d341aeaa7fb6f
languageName: node
linkType: hard
"napi-build-utils@npm:^1.0.1":
version: 1.0.2
resolution: "napi-build-utils@npm:1.0.2"