✨ Improve boards page, show if Public/Restricted
This commit is contained in:
@@ -44,12 +44,14 @@
|
||||
},
|
||||
"seeMore": "See more...",
|
||||
"position": {
|
||||
"left": "Left",
|
||||
"center": "Center",
|
||||
"right": "Right"
|
||||
"left": "Left",
|
||||
"center": "Center",
|
||||
"right": "Right"
|
||||
},
|
||||
"attributes": {
|
||||
"width": "Width",
|
||||
"height": "Height"
|
||||
}
|
||||
},
|
||||
"public": "Public",
|
||||
"restricted": "Restricted"
|
||||
}
|
||||
@@ -25,7 +25,6 @@ export default function BoardPage({
|
||||
|
||||
type BoardGetServerSideProps = {
|
||||
config: ConfigType;
|
||||
dockerEnabled: boolean;
|
||||
_nextI18Next?: SSRConfig['_nextI18Next'];
|
||||
};
|
||||
|
||||
|
||||
@@ -18,34 +18,35 @@ import {
|
||||
IconDeviceFloppy,
|
||||
IconDotsVertical,
|
||||
IconFolderFilled,
|
||||
IconLock,
|
||||
IconLockOff,
|
||||
IconPlus,
|
||||
IconStack,
|
||||
IconStarFilled,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react';
|
||||
import { createServerSideHelpers } from '@trpc/react-query/server';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import superjson from 'superjson';
|
||||
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
|
||||
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { boardRouter } from '~/server/api/routers/board';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { prisma } from '~/server/db';
|
||||
import { sleep } from '~/tools/client/time';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
const BoardsPage = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
// Infer return type from the `getServerSideProps` function
|
||||
export default function BoardsPage({
|
||||
boards,
|
||||
session,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { data, refetch } = api.boards.all.useQuery(undefined, {
|
||||
staleTime: 0,
|
||||
initialData: boards,
|
||||
cacheTime: 1000 * 60 * 5, // Cache for 5 minutes
|
||||
});
|
||||
const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({
|
||||
@@ -68,7 +69,7 @@ const BoardsPage = () => {
|
||||
|
||||
<Group position="apart">
|
||||
<Title mb="xl">{t('pageTitle')}</Title>
|
||||
{sessionData?.user.isAdmin && (
|
||||
{session?.user.isAdmin && (
|
||||
<Button
|
||||
onClick={openCreateBoardModal}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
@@ -79,166 +80,167 @@ const BoardsPage = () => {
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{data && (
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing="lg"
|
||||
breakpoints={[
|
||||
{ maxWidth: '62rem', cols: 2, spacing: 'lg' },
|
||||
{ maxWidth: '48rem', cols: 1, spacing: 'lg' },
|
||||
]}
|
||||
>
|
||||
{data.map((board, index) => (
|
||||
<Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder>
|
||||
<LoadingOverlay visible={deletingDashboards.includes(board.name)} />
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing="lg"
|
||||
breakpoints={[
|
||||
{ maxWidth: '62rem', cols: 2, spacing: 'lg' },
|
||||
{ maxWidth: '48rem', cols: 1, spacing: 'lg' },
|
||||
]}
|
||||
>
|
||||
{data.map((board, index) => (
|
||||
<Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder>
|
||||
<LoadingOverlay visible={deletingDashboards.includes(board.name)} />
|
||||
|
||||
<Group mb="xl" position="apart" noWrap>
|
||||
<Text weight={500} mb="xs">
|
||||
{board.name}
|
||||
</Text>
|
||||
<Group spacing="xs" noWrap>
|
||||
<Group mb="xl" position="apart" noWrap>
|
||||
<Text weight={500} mb="xs">
|
||||
{board.name}
|
||||
</Text>
|
||||
<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
|
||||
leftSection={<IconFolderFilled size=".7rem" />}
|
||||
color="pink"
|
||||
leftSection={<IconStarFilled size=".7rem" />}
|
||||
color="yellow"
|
||||
variant="light"
|
||||
>
|
||||
{t('cards.badges.fileSystem')}
|
||||
{t('cards.badges.default')}
|
||||
</Badge>
|
||||
{board.isDefaultForUser && (
|
||||
<Badge
|
||||
leftSection={<IconStarFilled size=".7rem" />}
|
||||
color="yellow"
|
||||
variant="light"
|
||||
>
|
||||
{t('cards.badges.default')}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Group position="apart">
|
||||
<Group spacing="xs">
|
||||
<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>
|
||||
</Group>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Group position="apart">
|
||||
<Group spacing="xs">
|
||||
<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>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</ManageLayout>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
const result = checkForSessionOrAskForLogin(ctx, session, () => true);
|
||||
if (result) {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getServerAuthSession({ req: context.req, res: context.res });
|
||||
const result = checkForSessionOrAskForLogin(
|
||||
context,
|
||||
session,
|
||||
() => session?.user.isAdmin == true
|
||||
);
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: boardRouter,
|
||||
ctx: {
|
||||
session,
|
||||
cookies: ctx.req.cookies,
|
||||
prisma: prisma,
|
||||
},
|
||||
transformer: superjson,
|
||||
const caller = boardRouter.createCaller({
|
||||
session: session,
|
||||
cookies: context.req.cookies,
|
||||
});
|
||||
|
||||
await helpers.all.prefetch();
|
||||
const boards = await caller.all();
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
manageNamespaces,
|
||||
ctx.locale,
|
||||
ctx.req,
|
||||
ctx.res
|
||||
context.locale,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
boards,
|
||||
session,
|
||||
...translations,
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default BoardsPage;
|
||||
|
||||
@@ -25,6 +25,7 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
return {
|
||||
name: name,
|
||||
allowGuests: config.settings.access.allowGuests,
|
||||
countApps: countApps,
|
||||
countWidgets: config.widgets.length,
|
||||
countCategories: config.categories.length,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
GetServerSideProps,
|
||||
GetServerSidePropsContext,
|
||||
GetServerSidePropsResult,
|
||||
PreviewData,
|
||||
Redirect
|
||||
} from 'next';
|
||||
|
||||
import { Session } from 'next-auth';
|
||||
@@ -13,13 +13,12 @@ export const checkForSessionOrAskForLogin = (
|
||||
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
|
||||
session: Session | null,
|
||||
accessCallback: () => boolean
|
||||
): GetServerSidePropsResult<any> | undefined => {
|
||||
): GetServerSidePropsResult<never> | undefined => {
|
||||
const permitted = accessCallback();
|
||||
|
||||
// user is logged in but does not have the required access
|
||||
if (session?.user && !permitted) {
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: '/401',
|
||||
permanent: false
|
||||
@@ -34,7 +33,6 @@ export const checkForSessionOrAskForLogin = (
|
||||
|
||||
// user is logged out and needs to sign in
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
|
||||
permanent: false,
|
||||
|
||||
Reference in New Issue
Block a user