Improve boards page, show if Public/Restricted

This commit is contained in:
ajnart
2023-11-13 20:28:31 +01:00
parent e3e890f0a1
commit 5a563b3875
5 changed files with 159 additions and 157 deletions

View File

@@ -44,12 +44,14 @@
}, },
"seeMore": "See more...", "seeMore": "See more...",
"position": { "position": {
"left": "Left", "left": "Left",
"center": "Center", "center": "Center",
"right": "Right" "right": "Right"
}, },
"attributes": { "attributes": {
"width": "Width", "width": "Width",
"height": "Height" "height": "Height"
} },
"public": "Public",
"restricted": "Restricted"
} }

View File

@@ -25,7 +25,6 @@ export default function BoardPage({
type BoardGetServerSideProps = { type BoardGetServerSideProps = {
config: ConfigType; config: ConfigType;
dockerEnabled: boolean;
_nextI18Next?: SSRConfig['_nextI18Next']; _nextI18Next?: SSRConfig['_nextI18Next'];
}; };

View File

@@ -18,34 +18,35 @@ import {
IconDeviceFloppy, IconDeviceFloppy,
IconDotsVertical, IconDotsVertical,
IconFolderFilled, IconFolderFilled,
IconLock,
IconLockOff,
IconPlus, IconPlus,
IconStack, IconStack,
IconStarFilled, IconStarFilled,
IconTrash, IconTrash,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { createServerSideHelpers } from '@trpc/react-query/server'; import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import { GetServerSideProps } from 'next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import superjson from 'superjson';
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal'; import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal'; import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { boardRouter } from '~/server/api/routers/board'; import { boardRouter } from '~/server/api/routers/board';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { prisma } from '~/server/db';
import { sleep } from '~/tools/client/time'; import { sleep } from '~/tools/client/time';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { manageNamespaces } from '~/tools/server/translation-namespaces'; import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
const BoardsPage = () => { // Infer return type from the `getServerSideProps` function
const { data: sessionData } = useSession(); export default function BoardsPage({
boards,
session,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data, refetch } = api.boards.all.useQuery(undefined, { const { data, refetch } = api.boards.all.useQuery(undefined, {
staleTime: 0, initialData: boards,
cacheTime: 1000 * 60 * 5, // Cache for 5 minutes cacheTime: 1000 * 60 * 5, // Cache for 5 minutes
}); });
const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({ const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({
@@ -68,7 +69,7 @@ const BoardsPage = () => {
<Group position="apart"> <Group position="apart">
<Title mb="xl">{t('pageTitle')}</Title> <Title mb="xl">{t('pageTitle')}</Title>
{sessionData?.user.isAdmin && ( {session?.user.isAdmin && (
<Button <Button
onClick={openCreateBoardModal} onClick={openCreateBoardModal}
leftIcon={<IconPlus size="1rem" />} leftIcon={<IconPlus size="1rem" />}
@@ -79,166 +80,167 @@ const BoardsPage = () => {
)} )}
</Group> </Group>
{data && ( <SimpleGrid
<SimpleGrid cols={3}
cols={3} spacing="lg"
spacing="lg" breakpoints={[
breakpoints={[ { maxWidth: '62rem', cols: 2, spacing: 'lg' },
{ maxWidth: '62rem', cols: 2, spacing: 'lg' }, { maxWidth: '48rem', cols: 1, spacing: 'lg' },
{ maxWidth: '48rem', cols: 1, spacing: 'lg' }, ]}
]} >
> {data.map((board, index) => (
{data.map((board, index) => ( <Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder>
<Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder> <LoadingOverlay visible={deletingDashboards.includes(board.name)} />
<LoadingOverlay visible={deletingDashboards.includes(board.name)} />
<Group mb="xl" position="apart" noWrap> <Group mb="xl" position="apart" noWrap>
<Text weight={500} mb="xs"> <Text weight={500} mb="xs">
{board.name} {board.name}
</Text> </Text>
<Group spacing="xs" noWrap> <Group spacing="xs" noWrap>
<Badge leftSection={<IconFolderFilled size=".7rem" />} color="pink" variant="light">
{t('cards.badges.fileSystem')}
</Badge>
<Badge
leftSection={
board.allowGuests ? <IconLock size=".7rem" /> : <IconLockOff size=".7rem" />
}
color="green"
variant="light"
>
{board.allowGuests ? t('common:public') : t('common:restricted')}
</Badge>
{board.isDefaultForUser && (
<Badge <Badge
leftSection={<IconFolderFilled size=".7rem" />} leftSection={<IconStarFilled size=".7rem" />}
color="pink" color="yellow"
variant="light" variant="light"
> >
{t('cards.badges.fileSystem')} {t('cards.badges.default')}
</Badge> </Badge>
{board.isDefaultForUser && ( )}
<Badge </Group>
leftSection={<IconStarFilled size=".7rem" />} </Group>
color="yellow"
variant="light" <Stack spacing={3}>
> <Group position="apart">
{t('cards.badges.default')} <Group spacing="xs">
</Badge> <IconBox opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.apps')}</Text>
</Group>
<Text>{board.countApps}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconStack opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.widgets')}</Text>
</Group>
<Text>{board.countWidgets}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconCategory opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.categories')}</Text>
</Group>
<Text>{board.countCategories}</Text>
</Group>
</Stack>
<Group mt="md">
<Button
component={Link}
style={{ flexGrow: 1 }}
variant="default"
color="blue"
radius="md"
href={`/board/${board.name}`}
>
{t('cards.buttons.view')}
</Button>
<Menu width={240} withinPortal position="bottom-end">
<Menu.Target>
<ActionIcon h={34} w={34} variant="default">
<IconDotsVertical size="1rem" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<IconDeviceFloppy size="1rem" />}
onClick={async () => {
void mutateAsync({
board: board.name,
});
}}
>
<Text size="sm">{t('cards.menu.setAsDefault')}</Text>
</Menu.Item>
{session?.user.isAdmin && (
<>
<Menu.Divider />
<Menu.Item
onClick={async () => {
openDeleteBoardModal({
boardName: board.name,
onConfirm: async () => {
append(board.name);
// give user feedback, that it's being deleted
await sleep(500);
filter((item, _) => item !== board.name);
},
});
}}
disabled={board.name === 'default'}
icon={<IconTrash size="1rem" />}
color="red"
>
<Text size="sm">{t('cards.menu.delete.label')}</Text>
{board.name === 'default' && (
<Text size="xs">{t('cards.menu.delete.disabled')}</Text>
)}
</Menu.Item>
</>
)} )}
</Group> </Menu.Dropdown>
</Group> </Menu>
</Group>
<Stack spacing={3}> </Card>
<Group position="apart"> ))}
<Group spacing="xs"> </SimpleGrid>
<IconBox opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.apps')}</Text>
</Group>
<Text>{board.countApps}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconStack opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.widgets')}</Text>
</Group>
<Text>{board.countWidgets}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconCategory opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.categories')}</Text>
</Group>
<Text>{board.countCategories}</Text>
</Group>
</Stack>
<Group mt="md">
<Button
component={Link}
style={{ flexGrow: 1 }}
variant="default"
color="blue"
radius="md"
href={`/board/${board.name}`}
>
{t('cards.buttons.view')}
</Button>
<Menu width={240} withinPortal position="bottom-end">
<Menu.Target>
<ActionIcon h={34} w={34} variant="default">
<IconDotsVertical size="1rem" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<IconDeviceFloppy size="1rem" />}
onClick={async () => {
void mutateAsync({
board: board.name,
});
}}
>
<Text size="sm">{t('cards.menu.setAsDefault')}</Text>
</Menu.Item>
{sessionData?.user.isAdmin && (
<>
<Menu.Divider />
<Menu.Item
onClick={async () => {
openDeleteBoardModal({
boardName: board.name,
onConfirm: async () => {
append(board.name);
// give user feedback, that it's being deleted
await sleep(500);
filter((item, _) => item !== board.name);
},
});
}}
disabled={board.name === 'default'}
icon={<IconTrash size="1rem" />}
color="red"
>
<Text size="sm">{t('cards.menu.delete.label')}</Text>
{board.name === 'default' && (
<Text size="xs">{t('cards.menu.delete.disabled')}</Text>
)}
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
</Group>
</Card>
))}
</SimpleGrid>
)}
</ManageLayout> </ManageLayout>
); );
}; }
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerAuthSession(ctx); const session = await getServerAuthSession({ req: context.req, res: context.res });
const result = checkForSessionOrAskForLogin(
const result = checkForSessionOrAskForLogin(ctx, session, () => true); context,
if (result) { session,
() => session?.user.isAdmin == true
);
if (result !== undefined) {
return result; return result;
} }
const helpers = createServerSideHelpers({ const caller = boardRouter.createCaller({
router: boardRouter, session: session,
ctx: { cookies: context.req.cookies,
session,
cookies: ctx.req.cookies,
prisma: prisma,
},
transformer: superjson,
}); });
await helpers.all.prefetch(); const boards = await caller.all();
const translations = await getServerSideTranslations( const translations = await getServerSideTranslations(
manageNamespaces, manageNamespaces,
ctx.locale, context.locale,
ctx.req, context.req,
ctx.res context.res
); );
return { return {
props: { props: {
boards,
session,
...translations, ...translations,
trpcState: helpers.dehydrate(),
}, },
}; };
}; };
export default BoardsPage;

View File

@@ -25,6 +25,7 @@ export const boardRouter = createTRPCRouter({
return { return {
name: name, name: name,
allowGuests: config.settings.access.allowGuests,
countApps: countApps, countApps: countApps,
countWidgets: config.widgets.length, countWidgets: config.widgets.length,
countCategories: config.categories.length, countCategories: config.categories.length,

View File

@@ -1,8 +1,8 @@
import { import {
GetServerSideProps,
GetServerSidePropsContext, GetServerSidePropsContext,
GetServerSidePropsResult, GetServerSidePropsResult,
PreviewData, PreviewData,
Redirect
} from 'next'; } from 'next';
import { Session } from 'next-auth'; import { Session } from 'next-auth';
@@ -13,13 +13,12 @@ export const checkForSessionOrAskForLogin = (
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>, context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
session: Session | null, session: Session | null,
accessCallback: () => boolean accessCallback: () => boolean
): GetServerSidePropsResult<any> | undefined => { ): GetServerSidePropsResult<never> | undefined => {
const permitted = accessCallback(); const permitted = accessCallback();
// user is logged in but does not have the required access // user is logged in but does not have the required access
if (session?.user && !permitted) { if (session?.user && !permitted) {
return { return {
props: {},
redirect: { redirect: {
destination: '/401', destination: '/401',
permanent: false permanent: false
@@ -34,7 +33,6 @@ export const checkForSessionOrAskForLogin = (
// user is logged out and needs to sign in // user is logged out and needs to sign in
return { return {
props: {},
redirect: { redirect: {
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`, destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
permanent: false, permanent: false,