mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-15 17:56:21 +01:00
feature: add trpc openapi (#1818)
This commit is contained in:
@@ -83,6 +83,7 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"drizzle-kit": "^0.19.13",
|
"drizzle-kit": "^0.19.13",
|
||||||
"drizzle-orm": "^0.28.6",
|
"drizzle-orm": "^0.28.6",
|
||||||
|
"drizzle-zod": "^0.5.1",
|
||||||
"fily-publish-gridstack": "^0.0.13",
|
"fily-publish-gridstack": "^0.0.13",
|
||||||
"flag-icons": "^6.9.2",
|
"flag-icons": "^6.9.2",
|
||||||
"framer-motion": "^10.0.0",
|
"framer-motion": "^10.0.0",
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
"next-auth": "^4.23.0",
|
"next-auth": "^4.23.0",
|
||||||
"next-i18next": "^14.0.0",
|
"next-i18next": "^14.0.0",
|
||||||
|
"nextjs-cors": "^2.2.0",
|
||||||
"nzbget-api": "^0.0.3",
|
"nzbget-api": "^0.0.3",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -105,6 +107,8 @@
|
|||||||
"react-simple-code-editor": "^0.13.1",
|
"react-simple-code-editor": "^0.13.1",
|
||||||
"rss-parser": "^3.12.0",
|
"rss-parser": "^3.12.0",
|
||||||
"sabnzbd-api": "^1.5.0",
|
"sabnzbd-api": "^1.5.0",
|
||||||
|
"swagger-ui-react": "^5.11.0",
|
||||||
|
"trpc-openapi": "^1.2.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"xml-js": "^1.6.11",
|
"xml-js": "^1.6.11",
|
||||||
"xss": "^1.0.14",
|
"xss": "^1.0.14",
|
||||||
@@ -122,6 +126,7 @@
|
|||||||
"@types/node": "18.17.8",
|
"@types/node": "18.17.8",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^18.2.11",
|
"@types/react": "^18.2.11",
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/umami": "^0.1.4",
|
"@types/umami": "^0.1.4",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@types/video.js": "^7.3.51",
|
"@types/video.js": "^7.3.51",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"title": "Tools",
|
"title": "Tools",
|
||||||
"items": {
|
"items": {
|
||||||
"docker": "Docker"
|
"docker": "Docker",
|
||||||
|
"api": "API"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { ActionIcon, Badge, Box, Group, Title, Text, Tooltip, Button } from '@mantine/core';
|
import { Badge, Box, Button, Group, Text, Title } from '@mantine/core';
|
||||||
import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal';
|
import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal';
|
||||||
import { IconUserDown, IconUserUp } from '@tabler/icons-react';
|
import { IconUserDown, IconUserUp } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { createSelectSchema } from 'drizzle-zod';
|
||||||
|
import { users } from '~/server/db/schema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const userWithoutSecrets = createSelectSchema(users).omit({
|
||||||
|
password: true,
|
||||||
|
salt: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const ManageUserRoles = ({ user }: {
|
export const ManageUserRoles = ({ user }: {
|
||||||
user: {
|
user: z.infer<typeof userWithoutSecrets>
|
||||||
image: string | null;
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
password: string | null;
|
|
||||||
email: string | null;
|
|
||||||
emailVerified: Date | null;
|
|
||||||
salt: string | null;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isOwner: boolean;
|
|
||||||
}
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation(['manage/users/edit', 'manage/users']);
|
const { t } = useTranslation(['manage/users/edit', 'manage/users']);
|
||||||
const { data: sessionData } = useSession();
|
const { data: sessionData } = useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box maw={500}>
|
<Box maw={500}>
|
||||||
<Title order={3}>
|
<Title order={3}>
|
||||||
@@ -33,7 +32,7 @@ export const ManageUserRoles = ({ user }: {
|
|||||||
|
|
||||||
{user.isAdmin ? (
|
{user.isAdmin ? (
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<IconUserDown size='1rem' />}
|
leftIcon={<IconUserDown size="1rem" />}
|
||||||
disabled={user.id === sessionData?.user?.id || user.isOwner}
|
disabled={user.id === sessionData?.user?.id || user.isOwner}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openRoleChangeModal({
|
openRoleChangeModal({
|
||||||
@@ -47,7 +46,7 @@ export const ManageUserRoles = ({ user }: {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<IconUserUp size='1rem' />}
|
leftIcon={<IconUserUp size="1rem" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openRoleChangeModal({
|
openRoleChangeModal({
|
||||||
name: user.name as string,
|
name: user.name as string,
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import {
|
|||||||
Indicator,
|
Indicator,
|
||||||
NavLink,
|
NavLink,
|
||||||
Navbar,
|
Navbar,
|
||||||
Paper,
|
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -21,10 +19,9 @@ import {
|
|||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconGitFork,
|
IconGitFork,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconInfoCircle,
|
|
||||||
IconInfoSmall,
|
IconInfoSmall,
|
||||||
IconLayoutDashboard,
|
IconLayoutDashboard,
|
||||||
IconMailForward,
|
IconMailForward, IconPlug,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconTool,
|
IconTool,
|
||||||
IconUser,
|
IconUser,
|
||||||
@@ -104,6 +101,10 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
icon: IconBrandDocker,
|
icon: IconBrandDocker,
|
||||||
href: '/manage/tools/docker',
|
href: '/manage/tools/docker',
|
||||||
},
|
},
|
||||||
|
api: {
|
||||||
|
icon: IconPlug,
|
||||||
|
href: '/manage/tools/swagger'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
|
|||||||
22
src/pages/api/[...trpc].ts
Normal file
22
src/pages/api/[...trpc].ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import cors from 'nextjs-cors';
|
||||||
|
import { createOpenApiNextHandler } from 'trpc-openapi';
|
||||||
|
import { createTRPCContext } from '~/server/api/trpc';
|
||||||
|
import { rootRouter } from '~/server/api/root';
|
||||||
|
import Consola from 'consola';
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Setup CORS
|
||||||
|
await cors(req, res);
|
||||||
|
|
||||||
|
// Handle incoming OpenAPI requests
|
||||||
|
return createOpenApiNextHandler({
|
||||||
|
router: rootRouter,
|
||||||
|
createContext: createTRPCContext,
|
||||||
|
onError({ error, path }) {
|
||||||
|
Consola.error(`tRPC OpenAPI error on ${path}: ${error}`);
|
||||||
|
}
|
||||||
|
})(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
9
src/pages/api/openapi.json.ts
Normal file
9
src/pages/api/openapi.json.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { openApiDocument } from '~/server/openai';
|
||||||
|
|
||||||
|
// Respond with our OpenAPI schema
|
||||||
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
res.status(200).send(openApiDocument);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
93
src/pages/manage/tools/swagger.tsx
Normal file
93
src/pages/manage/tools/swagger.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { GetServerSidePropsContext, NextPage } from 'next';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import 'swagger-ui-react/swagger-ui.css';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { ActionIcon, Button, Group, Text, TextInput, Title, Tooltip, useMantineTheme } from '@mantine/core';
|
||||||
|
import { IconCopy, IconLockAccess } from '@tabler/icons-react';
|
||||||
|
import { useClipboard, useDisclosure } from '@mantine/hooks';
|
||||||
|
import Cookies from 'cookies';
|
||||||
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
|
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||||
|
|
||||||
|
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
||||||
|
|
||||||
|
const SwaggerApiPage = ({ authenticationToken }: { authenticationToken: string }) => {
|
||||||
|
const [accessTokenRevealed, { toggle: toggleAccessTokenReveal, close: hideAccessToken }] = useDisclosure(false);
|
||||||
|
const clipboard = useClipboard({ timeout: 2500 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clipboard.copied) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAccessToken();
|
||||||
|
}, [clipboard.copied]);
|
||||||
|
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return <ManageLayout>
|
||||||
|
<Head>
|
||||||
|
<title>API • Homarr</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Title mb={'md'}>API</Title>
|
||||||
|
<Text mb={'xl'}>Advanced users can use the API to interface with Homarr. The documentation is completely local,
|
||||||
|
interactive
|
||||||
|
and complies with the Open API standard. Any compatible client can import for easy usage.</Text>
|
||||||
|
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button onClick={toggleAccessTokenReveal} leftIcon={<IconLockAccess size={'1rem'} />} variant={'light'}>
|
||||||
|
Show your personal access token
|
||||||
|
</Button>
|
||||||
|
{accessTokenRevealed && (
|
||||||
|
<TextInput
|
||||||
|
rightSection={
|
||||||
|
<Tooltip opened={clipboard.copied} label={"Copied"}>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.copy(authenticationToken);
|
||||||
|
}}>
|
||||||
|
<IconCopy size={'1rem'} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>}
|
||||||
|
value={authenticationToken} />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div data-color-scheme={theme.colorScheme} className={"open-api-container"}>
|
||||||
|
<SwaggerUI url="/api/openapi.json" />
|
||||||
|
</div>
|
||||||
|
</ManageLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
|
|
||||||
|
const session = await getServerAuthSession(ctx);
|
||||||
|
const result = checkForSessionOrAskForLogin(ctx, session, () => true);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a cookies instance
|
||||||
|
const cookies = new Cookies(ctx.req, ctx.res);
|
||||||
|
|
||||||
|
const authenticationToken = cookies.get('next-auth.session-token');
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
authenticationToken: authenticationToken,
|
||||||
|
...(await getServerSideTranslations(
|
||||||
|
['layout/manage', 'manage/index'],
|
||||||
|
ctx.locale,
|
||||||
|
ctx.req,
|
||||||
|
ctx.res,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SwaggerApiPage;
|
||||||
@@ -21,7 +21,7 @@ const EditPage = () => {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { isLoading, data } = api.user.details.useQuery({ userId: router.query.userId as string });
|
const { data } = api.user.details.useQuery({ userId: router.query.userId as string });
|
||||||
|
|
||||||
const metaTitle = `${t('metaTitle', {
|
const metaTitle = `${t('metaTitle', {
|
||||||
username: data?.name,
|
username: data?.name,
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ import * as https from 'https';
|
|||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
ping: publicProcedure
|
ping: publicProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/app/ping', tags: ['app'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
configName: z.string(),
|
configName: z.string(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({
|
||||||
|
status: z.number(),
|
||||||
|
statusText: z.string(),
|
||||||
|
state: z.string()
|
||||||
|
}))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const config = getConfig(input.configName);
|
const config = getConfig(input.configName);
|
||||||
const app = config.apps.find((app) => app.id === input.id);
|
const app = config.apps.find((app) => app.id === input.id);
|
||||||
@@ -62,7 +68,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
|
|
||||||
if (error.code === 'ECONNABORTED') {
|
if (error.code === 'ECONNABORTED') {
|
||||||
Consola.error(
|
Consola.error(
|
||||||
`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`
|
`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`,
|
||||||
);
|
);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'TIMEOUT',
|
code: 'TIMEOUT',
|
||||||
|
|||||||
@@ -13,7 +13,18 @@ import { writeConfig } from '~/tools/config/writeConfig';
|
|||||||
import { configNameSchema } from '~/validations/boards';
|
import { configNameSchema } from '~/validations/boards';
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/boards/all', tags: ['board'] } })
|
||||||
|
.input(z.void())
|
||||||
|
.output(z.array(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
allowGuests: z.boolean(),
|
||||||
|
countApps: z.number().min(0),
|
||||||
|
countWidgets: z.number().min(0),
|
||||||
|
countCategories: z.number().min(0),
|
||||||
|
isDefaultForUser: z.boolean(),
|
||||||
|
})))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
|
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
|
||||||
|
|
||||||
const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
|
const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
|
||||||
@@ -37,6 +48,8 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
addAppsForContainers: adminProcedure
|
addAppsForContainers: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'POST', path: '/boards/add-apps', tags: ['board'] } })
|
||||||
|
.output(z.void())
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardName: configNameSchema,
|
boardName: configNameSchema,
|
||||||
@@ -89,10 +102,12 @@ export const boardRouter = createTRPCRouter({
|
|||||||
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||||
}),
|
}),
|
||||||
renameBoard: protectedProcedure
|
renameBoard: protectedProcedure
|
||||||
|
.meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } })
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
oldName: z.string(),
|
oldName: z.string(),
|
||||||
newName: z.string().min(1),
|
newName: z.string().min(1),
|
||||||
}))
|
}))
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (input.oldName === 'default') {
|
if (input.oldName === 'default') {
|
||||||
Consola.error(`Attempted to rename default configuration. Aborted deletion.`);
|
Consola.error(`Attempted to rename default configuration. Aborted deletion.`);
|
||||||
@@ -127,9 +142,11 @@ export const boardRouter = createTRPCRouter({
|
|||||||
Consola.info(`Deleted ${input.oldName} from file system`);
|
Consola.info(`Deleted ${input.oldName} from file system`);
|
||||||
}),
|
}),
|
||||||
duplicateBoard: protectedProcedure
|
duplicateBoard: protectedProcedure
|
||||||
|
.meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } })
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
boardName: z.string(),
|
boardName: z.string(),
|
||||||
}))
|
}))
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (!configExists(input.boardName)) {
|
if (!configExists(input.boardName)) {
|
||||||
Consola.error(`Tried to duplicate ${input.boardName} but this configuration does not exist.`);
|
Consola.error(`Tried to duplicate ${input.boardName} but this configuration does not exist.`);
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
|||||||
|
|
||||||
export const configRouter = createTRPCRouter({
|
export const configRouter = createTRPCRouter({
|
||||||
delete: adminProcedure
|
delete: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'DELETE', path: '/configs', tags: ['config'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ message: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (input.name.toLowerCase() === 'default') {
|
if (input.name.toLowerCase() === 'default') {
|
||||||
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
|
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
|
||||||
@@ -160,11 +162,21 @@ export const configRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
byName: publicProcedure
|
byName: publicProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/configs/byName',
|
||||||
|
tags: ['config'],
|
||||||
|
deprecated: true,
|
||||||
|
summary: 'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.'
|
||||||
|
}
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<ConfigType>())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
if (!configExists(input.name)) {
|
if (!configExists(input.name)) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -177,6 +189,7 @@ export const configRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
saveCustomization: adminProcedure
|
saveCustomization: adminProcedure
|
||||||
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const previousConfig = getConfig(input.name);
|
const previousConfig = getConfig(input.name);
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
|
|||||||
@@ -4,35 +4,40 @@ import { LocalIconsRepository } from '~/tools/server/images/local-icons-reposito
|
|||||||
import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository';
|
import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository';
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { NormalizedIconRepositoryResult } from '~/tools/server/images/abstract-icons-repository';
|
||||||
|
|
||||||
export const IconRespositories = [
|
export const IconRespositories = [
|
||||||
new LocalIconsRepository(),
|
new LocalIconsRepository(),
|
||||||
new GitHubIconsRepository(
|
new GitHubIconsRepository(
|
||||||
GitHubIconsRepository.walkxcode,
|
GitHubIconsRepository.walkxcode,
|
||||||
'Walkxcode Dashboard Icons',
|
'Walkxcode Dashboard Icons',
|
||||||
'Walkxcode on Github'
|
'Walkxcode on Github',
|
||||||
),
|
),
|
||||||
new UnpkgIconsRepository(
|
new UnpkgIconsRepository(
|
||||||
UnpkgIconsRepository.tablerRepository,
|
UnpkgIconsRepository.tablerRepository,
|
||||||
'Tabler Icons',
|
'Tabler Icons',
|
||||||
'Tabler Icons - GitHub (MIT)'
|
'Tabler Icons - GitHub (MIT)',
|
||||||
),
|
),
|
||||||
new JsdelivrIconsRepository(
|
new JsdelivrIconsRepository(
|
||||||
JsdelivrIconsRepository.papirusRepository,
|
JsdelivrIconsRepository.papirusRepository,
|
||||||
'Papirus Icons',
|
'Papirus Icons',
|
||||||
'Papirus Development Team on GitHub (Apache 2.0)'
|
'Papirus Development Team on GitHub (Apache 2.0)',
|
||||||
),
|
),
|
||||||
new JsdelivrIconsRepository(
|
new JsdelivrIconsRepository(
|
||||||
JsdelivrIconsRepository.homelabSvgAssetsRepository,
|
JsdelivrIconsRepository.homelabSvgAssetsRepository,
|
||||||
'Homelab Svg Assets',
|
'Homelab Svg Assets',
|
||||||
'loganmarchione on GitHub (MIT)'
|
'loganmarchione on GitHub (MIT)',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const iconRouter = createTRPCRouter({
|
export const iconRouter = createTRPCRouter({
|
||||||
all: publicProcedure.query(async () => {
|
all: publicProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/icons', tags: ['icon'] } })
|
||||||
|
.input(z.void())
|
||||||
|
.output(z.array(z.custom<NormalizedIconRepositoryResult>()))
|
||||||
|
.query(async () => {
|
||||||
const fetches = IconRespositories.map((rep) => rep.fetch());
|
const fetches = IconRespositories.map((rep) => rep.fetch());
|
||||||
const data = await Promise.all(fetches);
|
return await Promise.all(fetches);
|
||||||
return data;
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import { z } from 'zod';
|
|||||||
import { db } from '~/server/db';
|
import { db } from '~/server/db';
|
||||||
import { invites } from '~/server/db/schema';
|
import { invites } from '~/server/db/schema';
|
||||||
|
|
||||||
import { adminProcedure, createTRPCRouter, publicProcedure } from '../../trpc';
|
import { adminProcedure, createTRPCRouter } from '../../trpc';
|
||||||
|
|
||||||
export const inviteRouter = createTRPCRouter({
|
export const inviteRouter = createTRPCRouter({
|
||||||
all: adminProcedure
|
all: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/invites', tags: ['invite'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.number().min(1).max(100).default(10),
|
limit: z.number().min(1).max(100).default(10),
|
||||||
page: z.number().min(0),
|
page: z.number().min(0),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.output(z.object({
|
||||||
|
invites: z.array(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
expires: z.date(),
|
||||||
|
creator: z.string().or(z.null()),
|
||||||
|
})),
|
||||||
|
countPages: z.number().min(0),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
const limit = input.limit;
|
const limit = input.limit;
|
||||||
const dbInvites = await db.query.invites.findMany({
|
const dbInvites = await db.query.invites.findMany({
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@@ -44,14 +53,20 @@ export const inviteRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'POST', path: '/invites', tags: ['invite'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
expiration: z
|
expiration: z
|
||||||
.date()
|
.date()
|
||||||
.min(dayjs().add(5, 'minutes').toDate())
|
.min(dayjs().add(5, 'minutes').toDate())
|
||||||
.max(dayjs().add(6, 'months').toDate()),
|
.max(dayjs().add(6, 'months').toDate()),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
expires: z.date(),
|
||||||
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const inviteToInsert = {
|
const inviteToInsert = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
@@ -67,7 +82,11 @@ export const inviteRouter = createTRPCRouter({
|
|||||||
expires: inviteToInsert.expires,
|
expires: inviteToInsert.expires,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
delete: adminProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
delete: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'DELETE', path: '/invites', tags: ['invite'] } })
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.output(z.void())
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
await db.delete(invites).where(eq(invites.id, input.id));
|
await db.delete(invites).where(eq(invites.id, input.id));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
updateSettingsValidationSchema,
|
updateSettingsValidationSchema,
|
||||||
} from '~/validations/user';
|
} from '~/validations/user';
|
||||||
import { PossibleRoleFilter } from '~/pages/manage/users';
|
import { PossibleRoleFilter } from '~/pages/manage/users';
|
||||||
|
import { createSelectSchema } from 'drizzle-zod';
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
|
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
|
||||||
@@ -41,6 +42,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
updatePassword: adminProcedure
|
updatePassword: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
@@ -48,6 +50,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
terminateExistingSessions: z.boolean(),
|
terminateExistingSessions: z.boolean(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.id, input.userId),
|
where: eq(users.id, input.userId),
|
||||||
@@ -81,7 +84,11 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, input.userId));
|
.where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
count: publicProcedure.query(async () => {
|
count: publicProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/users/count', tags: ['user'] } })
|
||||||
|
.input(z.void())
|
||||||
|
.output(z.number())
|
||||||
|
.query(async () => {
|
||||||
return await getTotalUserCountAsync();
|
return await getTotalUserCountAsync();
|
||||||
}),
|
}),
|
||||||
createFromInvite: publicProcedure
|
createFromInvite: publicProcedure
|
||||||
@@ -133,7 +140,9 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(userSettings.userId, ctx.session?.user?.id));
|
.where(eq(userSettings.userId, ctx.session?.user?.id));
|
||||||
}),
|
}),
|
||||||
changeRole: adminProcedure
|
changeRole: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'PUT', path: '/users/roles', tags: ['user'] } })
|
||||||
.input(z.object({ id: z.string(), type: z.enum(['promote', 'demote']) }))
|
.input(z.object({ id: z.string(), type: z.enum(['promote', 'demote']) }))
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.session?.user?.id === input.id) {
|
if (ctx.session?.user?.id === input.id) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -166,11 +175,13 @@ export const userRouter = createTRPCRouter({
|
|||||||
.where(eq(users.id, input.id));
|
.where(eq(users.id, input.id));
|
||||||
}),
|
}),
|
||||||
changeLanguage: protectedProcedure
|
changeLanguage: protectedProcedure
|
||||||
|
.meta({ openapi: { method: 'PUT', path: '/users/language', tags: ['user'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
language: z.string(),
|
language: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await db
|
await db
|
||||||
.update(userSettings)
|
.update(userSettings)
|
||||||
@@ -218,6 +229,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
makeDefaultDashboard: protectedProcedure
|
makeDefaultDashboard: protectedProcedure
|
||||||
|
.meta({ openapi: { method: 'POST', path: '/users/make-default-dashboard', tags: ['user'] } })
|
||||||
|
.output(z.void())
|
||||||
.input(z.object({ board: z.string() }))
|
.input(z.object({ board: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await db
|
await db
|
||||||
@@ -243,7 +256,20 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.output(z.object({
|
||||||
|
users: z.array(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().or(z.null()).optional(),
|
||||||
|
isAdmin: z.boolean(),
|
||||||
|
isOwner: z.boolean(),
|
||||||
|
})),
|
||||||
|
countPages: z.number().min(0),
|
||||||
|
stats: z.object({
|
||||||
|
roles: z.record(z.number()),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
|
||||||
const roleFilter = () => {
|
const roleFilter = () => {
|
||||||
if (input.search.role === PossibleRoleFilter[1].id) {
|
if (input.search.role === PossibleRoleFilter[1].id) {
|
||||||
@@ -309,30 +335,54 @@ export const userRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
create: adminProcedure.input(createNewUserSchema).mutation(async ({ input }) => {
|
create: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'POST', path: '/users', tags: ['user'] } })
|
||||||
|
.input(createNewUserSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
await createUserIfNotPresent(input);
|
await createUserIfNotPresent(input);
|
||||||
}),
|
}),
|
||||||
details: adminProcedure.input(z.object({ userId: z.string() })).query(async ({ input }) => {
|
details: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'GET', path: '/users/getById', tags: ['user'] } })
|
||||||
|
.input(z.object({ userId: z.string() }))
|
||||||
|
.output(
|
||||||
|
createSelectSchema(users)
|
||||||
|
.omit({
|
||||||
|
password: true,
|
||||||
|
salt: true,
|
||||||
|
})
|
||||||
|
.optional())
|
||||||
|
.query(async ({ input }) => {
|
||||||
return db.query.users.findFirst({
|
return db.query.users.findFirst({
|
||||||
where: eq(users.id, input.userId),
|
where: eq(users.id, input.userId),
|
||||||
|
columns: {
|
||||||
|
password: false,
|
||||||
|
salt: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
updateDetails: adminProcedure.input(z.object({
|
updateDetails: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'PUT', path: '/users/details', tags: ['user'] } })
|
||||||
|
.input(z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value),
|
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value),
|
||||||
})).mutation(async ({ input }) => {
|
}))
|
||||||
|
.output(z.void())
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
await db.update(users).set({
|
await db.update(users).set({
|
||||||
name: input.username,
|
name: input.username,
|
||||||
email: input.eMail as string | null,
|
email: input.eMail as string | null,
|
||||||
}).where(eq(users.id, input.userId));
|
}).where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
deleteUser: adminProcedure
|
deleteUser: adminProcedure
|
||||||
|
.meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.id, input.id),
|
where: eq(users.id, input.id),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import superjson from 'superjson';
|
|||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
import { getServerAuthSession } from '../auth';
|
import { getServerAuthSession } from '../auth';
|
||||||
|
import { OpenApiMeta } from 'trpc-openapi';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
@@ -70,7 +71,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
* errors on the backend.
|
* errors on the backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
const t = initTRPC.context<typeof createTRPCContext>().meta<OpenApiMeta>().create({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
errorFormatter({ shape, error }) {
|
errorFormatter({ shape, error }) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
11
src/server/openai.ts
Normal file
11
src/server/openai.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { generateOpenApiDocument } from 'trpc-openapi';
|
||||||
|
import { appRouter } from '~/server/api/routers/app';
|
||||||
|
import { rootRouter } from '~/server/api/root';
|
||||||
|
|
||||||
|
export const openApiDocument = generateOpenApiDocument(rootRouter, {
|
||||||
|
title: 'Homarr API',
|
||||||
|
description: 'OpenAPI compliant REST API built of interfacing with Homarr',
|
||||||
|
version: '1.0.0',
|
||||||
|
baseUrl: 'http://localhost:3000/api',
|
||||||
|
docsUrl: 'https://homarr.dev'
|
||||||
|
});
|
||||||
@@ -21,63 +21,95 @@
|
|||||||
|
|
||||||
// Styling for grid-stack main area
|
// Styling for grid-stack main area
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
.grid-stack > .grid-stack-item[gs-w="#{$i}"] {
|
||||||
.grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
width: calc(100% / #{var(--gridstack-column-count)} * #{$i})
|
||||||
.grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
}
|
||||||
|
.grid-stack > .grid-stack-item[gs-min-w="#{$i}"] {
|
||||||
|
min-width: calc(100% / #{var(--gridstack-column-count)} * #{$i})
|
||||||
|
}
|
||||||
|
.grid-stack > .grid-stack-item[gs-max-w="#{$i}"] {
|
||||||
|
max-width: calc(100% / #{var(--gridstack-column-count)} * #{$i})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack>.grid-stack-item[gs-h="#{$i}"] { height: calc(#{$i}px * #{var(--gridstack-widget-width)}) }
|
.grid-stack > .grid-stack-item[gs-h="#{$i}"] {
|
||||||
.grid-stack>.grid-stack-item[gs-min-h="#{$i}"] { min-height: calc(#{$i}px * #{var(--gridstack-widget-width)}) }
|
height: calc(#{$i}px * #{var(--gridstack-widget-width)})
|
||||||
.grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: calc(#{$i}px * #{var(--gridstack-widget-width)}) }
|
}
|
||||||
|
.grid-stack > .grid-stack-item[gs-min-h="#{$i}"] {
|
||||||
|
min-height: calc(#{$i}px * #{var(--gridstack-widget-width)})
|
||||||
|
}
|
||||||
|
.grid-stack > .grid-stack-item[gs-max-h="#{$i}"] {
|
||||||
|
max-height: calc(#{$i}px * #{var(--gridstack-widget-width)})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: calc(100% / #{var(--gridstack-column-count)} * #{$i}) }
|
.grid-stack > .grid-stack-item[gs-x="#{$i}"] {
|
||||||
|
left: calc(100% / #{var(--gridstack-column-count)} * #{$i})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack>.grid-stack-item[gs-y="#{$i}"] { top: calc(#{$i}px * #{var(--gridstack-widget-width)}) }
|
.grid-stack > .grid-stack-item[gs-y="#{$i}"] {
|
||||||
|
top: calc(#{$i}px * #{var(--gridstack-widget-width)})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-stack>.grid-stack-item {
|
.grid-stack > .grid-stack-item {
|
||||||
min-width: #{var(--gridstack-widget-width)};
|
min-width: #{var(--gridstack-widget-width)};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styling for sidebar grid-stack elements
|
// Styling for sidebar grid-stack elements
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-w="#{$i}"] { width: 128px * $i }
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-w="#{$i}"] {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-min-w="#{$i}"] { min-width: 128px * $i }
|
width: 128px * $i
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-max-w="#{$i}"] { max-width: 128px * $i }
|
}
|
||||||
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-min-w="#{$i}"] {
|
||||||
|
min-width: 128px * $i
|
||||||
|
}
|
||||||
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-max-w="#{$i}"] {
|
||||||
|
max-width: 128px * $i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-h="#{$i}"] { height: 128px * $i }
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-h="#{$i}"] {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-min-h="#{$i}"] { min-height: 128px * $i }
|
height: 128px * $i
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-max-h="#{$i}"] { max-height: 128px * $i }
|
}
|
||||||
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-min-h="#{$i}"] {
|
||||||
|
min-height: 128px * $i
|
||||||
|
}
|
||||||
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-max-h="#{$i}"] {
|
||||||
|
max-height: 128px * $i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 to 3 {
|
@for $i from 1 to 3 {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-x="#{$i}"] { left: 128px * $i }
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-x="#{$i}"] {
|
||||||
|
left: 128px * $i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@for $i from 1 to 96 {
|
@for $i from 1 to 96 {
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-y="#{$i}"] { top: 128px * $i }
|
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-y="#{$i}"] {
|
||||||
|
top: 128px * $i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-stack.grid-stack-sidebar>.grid-stack-item {
|
.grid-stack.grid-stack-sidebar > .grid-stack-item {
|
||||||
min-width: 128px;
|
min-width: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// General gridstack styling
|
// General gridstack styling
|
||||||
.grid-stack>.grid-stack-item>.grid-stack-item-content,
|
.grid-stack > .grid-stack-item > .grid-stack-item-content,
|
||||||
.grid-stack>.grid-stack-item>.placeholder-content {
|
.grid-stack > .grid-stack-item > .placeholder-content {
|
||||||
inset: 10px;
|
inset: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-stack>.grid-stack-item>.ui-resizable-se {
|
.grid-stack > .grid-stack-item > .ui-resizable-se {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
@@ -115,6 +147,7 @@
|
|||||||
|
|
||||||
ul[data-type="taskList"] {
|
ul[data-type="taskList"] {
|
||||||
padding-left: 17px;
|
padding-left: 17px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -124,6 +157,7 @@
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
&.ProseMirror-selectednode {
|
&.ProseMirror-selectednode {
|
||||||
outline: 3px solid rgba(0, 65, 198, 0.8);
|
outline: 3px solid rgba(0, 65, 198, 0.8);
|
||||||
}
|
}
|
||||||
@@ -181,3 +215,37 @@
|
|||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open-api-container[data-color-scheme="dark"] .swagger-ui {
|
||||||
|
select {
|
||||||
|
background-color: #25262b;
|
||||||
|
color: #82858e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheme-container {
|
||||||
|
background-color: #1a1b1e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opblock-tag,
|
||||||
|
.info .title {
|
||||||
|
color: #bfc5d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.servers-title,
|
||||||
|
.opblock .opblock-summary-operation-id,
|
||||||
|
.opblock .opblock-summary-path,
|
||||||
|
.opblock .opblock-summary-path__deprecated,
|
||||||
|
.info li, .info p, .info table {
|
||||||
|
color: #82858e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-methods svg,
|
||||||
|
.expand-operation svg,
|
||||||
|
.opblock-control-arrow svg {
|
||||||
|
fill: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opblock-summary-description {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user