✨ Improve boards page, show if Public/Restricted
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,6 @@ export default function BoardPage({
|
|||||||
|
|
||||||
type BoardGetServerSideProps = {
|
type BoardGetServerSideProps = {
|
||||||
config: ConfigType;
|
config: ConfigType;
|
||||||
dockerEnabled: boolean;
|
|
||||||
_nextI18Next?: SSRConfig['_nextI18Next'];
|
_nextI18Next?: SSRConfig['_nextI18Next'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user