feature: add trpc openapi (#1818)

This commit is contained in:
Manuel
2024-01-14 22:20:51 +01:00
committed by GitHub
parent 33da630db5
commit c701f723cf
18 changed files with 2177 additions and 134 deletions

View File

@@ -83,6 +83,7 @@
"dotenv": "^16.3.1",
"drizzle-kit": "^0.19.13",
"drizzle-orm": "^0.28.6",
"drizzle-zod": "^0.5.1",
"fily-publish-gridstack": "^0.0.13",
"flag-icons": "^6.9.2",
"framer-motion": "^10.0.0",
@@ -97,6 +98,7 @@
"next": "13.4.12",
"next-auth": "^4.23.0",
"next-i18next": "^14.0.0",
"nextjs-cors": "^2.2.0",
"nzbget-api": "^0.0.3",
"prismjs": "^1.29.0",
"react": "^18.2.0",
@@ -105,6 +107,8 @@
"react-simple-code-editor": "^0.13.1",
"rss-parser": "^3.12.0",
"sabnzbd-api": "^1.5.0",
"swagger-ui-react": "^5.11.0",
"trpc-openapi": "^1.2.0",
"uuid": "^9.0.0",
"xml-js": "^1.6.11",
"xss": "^1.0.14",
@@ -122,6 +126,7 @@
"@types/node": "18.17.8",
"@types/prismjs": "^1.26.0",
"@types/react": "^18.2.11",
"@types/swagger-ui-react": "^4.18.3",
"@types/umami": "^0.1.4",
"@types/uuid": "^9.0.0",
"@types/video.js": "^7.3.51",

View File

@@ -25,7 +25,8 @@
"tools": {
"title": "Tools",
"items": {
"docker": "Docker"
"docker": "Docker",
"api": "API"
}
},
"about": {

View File

@@ -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 { IconUserDown, IconUserUp } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
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 }: {
user: {
image: string | null;
id: string;
name: string | null;
password: string | null;
email: string | null;
emailVerified: Date | null;
salt: string | null;
isAdmin: boolean;
isOwner: boolean;
}
user: z.infer<typeof userWithoutSecrets>
}) => {
const { t } = useTranslation(['manage/users/edit', 'manage/users']);
const { data: sessionData } = useSession();
return (
<Box maw={500}>
<Title order={3}>
@@ -33,7 +32,7 @@ export const ManageUserRoles = ({ user }: {
{user.isAdmin ? (
<Button
leftIcon={<IconUserDown size='1rem' />}
leftIcon={<IconUserDown size="1rem" />}
disabled={user.id === sessionData?.user?.id || user.isOwner}
onClick={() => {
openRoleChangeModal({
@@ -47,7 +46,7 @@ export const ManageUserRoles = ({ user }: {
</Button>
) : (
<Button
leftIcon={<IconUserUp size='1rem' />}
leftIcon={<IconUserUp size="1rem" />}
onClick={() => {
openRoleChangeModal({
name: user.name as string,

View File

@@ -8,10 +8,8 @@ import {
Indicator,
NavLink,
Navbar,
Paper,
Text,
ThemeIcon,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
@@ -21,10 +19,9 @@ import {
IconBrandGithub,
IconGitFork,
IconHome,
IconInfoCircle,
IconInfoSmall,
IconLayoutDashboard,
IconMailForward,
IconMailForward, IconPlug,
IconQuestionMark,
IconTool,
IconUser,
@@ -104,6 +101,10 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
icon: IconBrandDocker,
href: '/manage/tools/docker',
},
api: {
icon: IconPlug,
href: '/manage/tools/swagger'
}
},
},
help: {

View 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;

View 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;

View 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;

View File

@@ -21,7 +21,7 @@ const EditPage = () => {
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', {
username: data?.name,

View File

@@ -11,12 +11,18 @@ import * as https from 'https';
export const appRouter = createTRPCRouter({
ping: publicProcedure
.meta({ openapi: { method: 'GET', path: '/app/ping', tags: ['app'] } })
.input(
z.object({
id: z.string(),
configName: z.string(),
})
}),
)
.output(z.object({
status: z.number(),
statusText: z.string(),
state: z.string()
}))
.query(async ({ input }) => {
const config = getConfig(input.configName);
const app = config.apps.find((app) => app.id === input.id);
@@ -62,7 +68,7 @@ export const appRouter = createTRPCRouter({
if (error.code === 'ECONNABORTED') {
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({
code: 'TIMEOUT',

View File

@@ -13,7 +13,18 @@ import { writeConfig } from '~/tools/config/writeConfig';
import { configNameSchema } from '~/validations/boards';
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 defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
@@ -37,6 +48,8 @@ export const boardRouter = createTRPCRouter({
);
}),
addAppsForContainers: adminProcedure
.meta({ openapi: { method: 'POST', path: '/boards/add-apps', tags: ['board'] } })
.output(z.void())
.input(
z.object({
boardName: configNameSchema,
@@ -89,10 +102,12 @@ export const boardRouter = createTRPCRouter({
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
}),
renameBoard: protectedProcedure
.meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } })
.input(z.object({
oldName: z.string(),
newName: z.string().min(1),
}))
.output(z.void())
.mutation(async ({ input }) => {
if (input.oldName === 'default') {
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`);
}),
duplicateBoard: protectedProcedure
.meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } })
.input(z.object({
boardName: z.string(),
}))
.output(z.void())
.mutation(async ({ input }) => {
if (!configExists(input.boardName)) {
Consola.error(`Tried to duplicate ${input.boardName} but this configuration does not exist.`);

View File

@@ -15,11 +15,13 @@ import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
export const configRouter = createTRPCRouter({
delete: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/configs', tags: ['config'] } })
.input(
z.object({
name: configNameSchema,
}),
)
.output(z.object({ message: z.string() }))
.mutation(async ({ input }) => {
if (input.name.toLowerCase() === 'default') {
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
@@ -160,11 +162,21 @@ export const configRouter = createTRPCRouter({
};
}),
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(
z.object({
name: configNameSchema,
}),
)
.output(z.custom<ConfigType>())
.query(async ({ ctx, input }) => {
if (!configExists(input.name)) {
throw new TRPCError({
@@ -177,6 +189,7 @@ export const configRouter = createTRPCRouter({
}),
saveCustomization: adminProcedure
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
.output(z.void())
.mutation(async ({ input }) => {
const previousConfig = getConfig(input.name);
const newConfig = {

View File

@@ -4,35 +4,40 @@ import { LocalIconsRepository } from '~/tools/server/images/local-icons-reposito
import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { z } from 'zod';
import { NormalizedIconRepositoryResult } from '~/tools/server/images/abstract-icons-repository';
export const IconRespositories = [
new LocalIconsRepository(),
new GitHubIconsRepository(
GitHubIconsRepository.walkxcode,
'Walkxcode Dashboard Icons',
'Walkxcode on Github'
'Walkxcode on Github',
),
new UnpkgIconsRepository(
UnpkgIconsRepository.tablerRepository,
'Tabler Icons',
'Tabler Icons - GitHub (MIT)'
'Tabler Icons - GitHub (MIT)',
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.papirusRepository,
'Papirus Icons',
'Papirus Development Team on GitHub (Apache 2.0)'
'Papirus Development Team on GitHub (Apache 2.0)',
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.homelabSvgAssetsRepository,
'Homelab Svg Assets',
'loganmarchione on GitHub (MIT)'
'loganmarchione on GitHub (MIT)',
),
];
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 data = await Promise.all(fetches);
return data;
return await Promise.all(fetches);
}),
});

View File

@@ -5,17 +5,26 @@ import { z } from 'zod';
import { db } from '~/server/db';
import { invites } from '~/server/db/schema';
import { adminProcedure, createTRPCRouter, publicProcedure } from '../../trpc';
import { adminProcedure, createTRPCRouter } from '../../trpc';
export const inviteRouter = createTRPCRouter({
all: adminProcedure
.meta({ openapi: { method: 'GET', path: '/invites', tags: ['invite'] } })
.input(
z.object({
limit: z.number().min(1).max(100).default(10),
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 dbInvites = await db.query.invites.findMany({
limit: limit,
@@ -44,14 +53,20 @@ export const inviteRouter = createTRPCRouter({
};
}),
create: adminProcedure
.meta({ openapi: { method: 'POST', path: '/invites', tags: ['invite'] } })
.input(
z.object({
expiration: z
.date()
.min(dayjs().add(5, 'minutes').toDate())
.max(dayjs().add(6, 'months').toDate()),
})
}),
)
.output(z.object({
id: z.string(),
token: z.string(),
expires: z.date(),
}))
.mutation(async ({ ctx, input }) => {
const inviteToInsert = {
id: randomUUID(),
@@ -67,7 +82,11 @@ export const inviteRouter = createTRPCRouter({
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));
}),
});

View File

@@ -22,6 +22,7 @@ import {
updateSettingsValidationSchema,
} from '~/validations/user';
import { PossibleRoleFilter } from '~/pages/manage/users';
import { createSelectSchema } from 'drizzle-zod';
export const userRouter = createTRPCRouter({
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
@@ -41,6 +42,7 @@ export const userRouter = createTRPCRouter({
});
}),
updatePassword: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } })
.input(
z.object({
userId: z.string(),
@@ -48,6 +50,7 @@ export const userRouter = createTRPCRouter({
terminateExistingSessions: z.boolean(),
}),
)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, input.userId),
@@ -81,7 +84,11 @@ export const userRouter = createTRPCRouter({
})
.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();
}),
createFromInvite: publicProcedure
@@ -133,7 +140,9 @@ export const userRouter = createTRPCRouter({
.where(eq(userSettings.userId, ctx.session?.user?.id));
}),
changeRole: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/roles', tags: ['user'] } })
.input(z.object({ id: z.string(), type: z.enum(['promote', 'demote']) }))
.output(z.void())
.mutation(async ({ ctx, input }) => {
if (ctx.session?.user?.id === input.id) {
throw new TRPCError({
@@ -166,11 +175,13 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, input.id));
}),
changeLanguage: protectedProcedure
.meta({ openapi: { method: 'PUT', path: '/users/language', tags: ['user'] } })
.input(
z.object({
language: z.string(),
}),
)
.output(z.void())
.mutation(async ({ ctx, input }) => {
await db
.update(userSettings)
@@ -218,6 +229,8 @@ export const userRouter = createTRPCRouter({
}),
makeDefaultDashboard: protectedProcedure
.meta({ openapi: { method: 'POST', path: '/users/make-default-dashboard', tags: ['user'] } })
.output(z.void())
.input(z.object({ board: z.string() }))
.mutation(async ({ ctx, input }) => {
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 = () => {
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);
}),
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({
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(),
username: z.string(),
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({
name: input.username,
email: input.eMail as string | null,
}).where(eq(users.id, input.userId));
}),
deleteUser: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } })
.input(
z.object({
id: z.string(),
}),
)
.output(z.void())
.mutation(async ({ ctx, input }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, input.id),

View File

@@ -13,6 +13,7 @@ import superjson from 'superjson';
import { ZodError } from 'zod';
import { getServerAuthSession } from '../auth';
import { OpenApiMeta } from 'trpc-openapi';
/**
* 1. CONTEXT
@@ -70,7 +71,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
const t = initTRPC.context<typeof createTRPCContext>().meta<OpenApiMeta>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {

11
src/server/openai.ts Normal file
View 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'
});

View File

@@ -21,24 +21,40 @@
// Styling for grid-stack main area
@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-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}) }
.grid-stack > .grid-stack-item[gs-w="#{$i}"] {
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 {
.grid-stack>.grid-stack-item[gs-h="#{$i}"] { 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)}) }
.grid-stack > .grid-stack-item[gs-h="#{$i}"] {
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 {
.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 {
.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 {
@@ -47,24 +63,40 @@
// Styling for sidebar grid-stack elements
@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-min-w="#{$i}"] { min-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-w="#{$i}"] {
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 {
.grid-stack.grid-stack-sidebar>.grid-stack-item[gs-h="#{$i}"] { 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 }
.grid-stack.grid-stack-sidebar > .grid-stack-item[gs-h="#{$i}"] {
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 {
.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 {
.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 {
@@ -115,6 +147,7 @@
ul[data-type="taskList"] {
padding-left: 17px;
li {
list-style-type: none;
display: flex;
@@ -124,6 +157,7 @@
img {
max-width: 100%;
&.ProseMirror-selectednode {
outline: 3px solid rgba(0, 65, 198, 0.8);
}
@@ -181,3 +215,37 @@
padding: 1rem 0;
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;
}
}

1785
yarn.lock

File diff suppressed because it is too large Load Diff