mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-01 20:29:17 +01:00
fix(management): improve homepage (#4260)
This commit is contained in:
@@ -2,6 +2,7 @@ import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { splitToNChunks } from "@homarr/common";
|
||||
import { integrationDefs } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import classes from "./hero-banner.module.css";
|
||||
|
||||
@@ -12,19 +13,20 @@ const icons = Object.values(integrationDefs)
|
||||
const countIconGroups = 3;
|
||||
const animationDurationInSeconds = icons.length;
|
||||
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
||||
const gridSpan = 12 / countIconGroups;
|
||||
|
||||
export const HeroBanner = () => {
|
||||
const gridSpan = 12 / countIconGroups;
|
||||
export const HeroBanner = async () => {
|
||||
const t = await getScopedI18n("management.page.home");
|
||||
|
||||
return (
|
||||
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
|
||||
<Stack gap={0}>
|
||||
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
|
||||
Welcome back to your
|
||||
{t("heroBanner.title")}
|
||||
</Title>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
|
||||
<Title fz={{ base: "h3", md: "h1" }}>Homarr Board</Title>
|
||||
<Title fz={{ base: "h3", md: "h1" }}>{t("heroBanner.subtitle", { app: "Homarr" })}</Title>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { HeroBanner } from "./_components/hero-banner";
|
||||
|
||||
interface LinkProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
count: number;
|
||||
href: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export async function generateMetadata() {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getScopedI18n("management");
|
||||
|
||||
return {
|
||||
@@ -29,78 +20,32 @@ export async function generateMetadata() {
|
||||
|
||||
export default async function ManagementPage() {
|
||||
const statistics = await api.home.getStats();
|
||||
const session = await auth();
|
||||
const t = await getScopedI18n("management.page.home");
|
||||
|
||||
const links: LinkProps[] = [
|
||||
{
|
||||
count: statistics.countBoards,
|
||||
href: "/manage/boards",
|
||||
subtitle: t("statisticLabel.boards"),
|
||||
title: t("statistic.board"),
|
||||
},
|
||||
{
|
||||
count: statistics.countUsers,
|
||||
href: "/manage/users",
|
||||
subtitle: t("statisticLabel.authentication"),
|
||||
title: t("statistic.user"),
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
count: statistics.countInvites,
|
||||
href: "/manage/users/invites",
|
||||
subtitle: t("statisticLabel.authentication"),
|
||||
title: t("statistic.invite"),
|
||||
hidden: !isProviderEnabled("credentials") || !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
count: statistics.countIntegrations,
|
||||
href: "/manage/integrations",
|
||||
subtitle: t("statisticLabel.resources"),
|
||||
title: t("statistic.integration"),
|
||||
},
|
||||
{
|
||||
count: statistics.countApps,
|
||||
href: "/manage/apps",
|
||||
subtitle: t("statisticLabel.resources"),
|
||||
title: t("statistic.app"),
|
||||
hidden: !session?.user,
|
||||
},
|
||||
{
|
||||
count: statistics.countGroups,
|
||||
href: "/manage/users/groups",
|
||||
subtitle: t("statisticLabel.authorization"),
|
||||
title: t("statistic.group"),
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<HeroBanner />
|
||||
<Space h="md" />
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{links.map(
|
||||
(link) =>
|
||||
!link.hidden && (
|
||||
<Card component={Link} href={link.href} key={link.href} radius="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Text size="2.4rem" fw="bolder">
|
||||
{link.count}
|
||||
</Text>
|
||||
<Stack gap={0}>
|
||||
<Text c="red" size="xs">
|
||||
{link.subtitle}
|
||||
</Text>
|
||||
<Text fw="bold">{link.title}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
{statistics.map((statistic) => (
|
||||
<Card component={Link} href={statistic.path} key={statistic.path} radius="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Text size="2.4rem" fw="bolder">
|
||||
{statistic.count}
|
||||
</Text>
|
||||
<Stack gap={0}>
|
||||
<Text c="red" size="xs">
|
||||
{t(`statisticLabel.${statistic.subtitleKey}`)}
|
||||
</Text>
|
||||
<Text fw="bold">{t(`statistic.${statistic.titleKey}`)}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
|
||||
@@ -1,31 +1,147 @@
|
||||
import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema";
|
||||
import { db, eq, inArray, or } from "@homarr/db";
|
||||
import {
|
||||
apps,
|
||||
boards,
|
||||
boardUserPermissions,
|
||||
groupMembers,
|
||||
groups,
|
||||
integrations,
|
||||
invites,
|
||||
medias,
|
||||
searchEngines,
|
||||
users,
|
||||
} from "@homarr/db/schema";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
interface HomeStatistic {
|
||||
titleKey: keyof TranslationObject["management"]["page"]["home"]["statistic"];
|
||||
subtitleKey: keyof TranslationObject["management"]["page"]["home"]["statisticLabel"];
|
||||
count: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const homeRouter = createTRPCRouter({
|
||||
getStats: publicProcedure.query(async ({ ctx }) => {
|
||||
const isAdmin = ctx.session?.user.permissions.includes("admin") ?? false;
|
||||
const isCredentialsEnabled = isProviderEnabled("credentials");
|
||||
|
||||
return {
|
||||
countBoards: await getCountForTableAsync(ctx.db, boards, true),
|
||||
countUsers: await getCountForTableAsync(ctx.db, users, isAdmin),
|
||||
countGroups: await getCountForTableAsync(ctx.db, groups, true),
|
||||
countInvites: await getCountForTableAsync(ctx.db, invites, isAdmin),
|
||||
countIntegrations: await getCountForTableAsync(ctx.db, integrations, isCredentialsEnabled && isAdmin),
|
||||
countApps: await getCountForTableAsync(ctx.db, apps, true),
|
||||
};
|
||||
const statistics: HomeStatistic[] = [];
|
||||
|
||||
const boardIds: string[] = [];
|
||||
if (ctx.session?.user && !ctx.session.user.permissions.includes("board-view-all")) {
|
||||
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.userId, ctx.session.user.id),
|
||||
});
|
||||
|
||||
const permissionsOfCurrentUserGroupsWhenPresent = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, ctx.session.user.id),
|
||||
with: {
|
||||
group: {
|
||||
with: {
|
||||
boardPermissions: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
boardIds.push(
|
||||
...permissionsOfCurrentUserWhenPresent
|
||||
.map((permission) => permission.boardId)
|
||||
.concat(
|
||||
permissionsOfCurrentUserGroupsWhenPresent
|
||||
.map((groupMember) => groupMember.group.boardPermissions.map((permission) => permission.boardId))
|
||||
.flat(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
statistics.push({
|
||||
titleKey: "board",
|
||||
subtitleKey: "boards",
|
||||
count: await db.$count(
|
||||
boards,
|
||||
ctx.session?.user.permissions.includes("board-view-all")
|
||||
? undefined
|
||||
: or(
|
||||
eq(boards.isPublic, true),
|
||||
eq(boards.creatorId, ctx.session?.user.id ?? ""),
|
||||
boardIds.length > 0 ? inArray(boards.id, boardIds) : undefined,
|
||||
),
|
||||
),
|
||||
path: "/manage/boards",
|
||||
});
|
||||
|
||||
if (isAdmin) {
|
||||
statistics.push({
|
||||
titleKey: "user",
|
||||
subtitleKey: "authentication",
|
||||
count: await db.$count(users),
|
||||
path: "/manage/users",
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdmin && isCredentialsEnabled) {
|
||||
statistics.push({
|
||||
titleKey: "invite",
|
||||
subtitleKey: "authentication",
|
||||
count: await db.$count(invites),
|
||||
path: "/manage/users/invites",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("integration-create")) {
|
||||
statistics.push({
|
||||
titleKey: "integration",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(integrations),
|
||||
path: "/manage/integrations",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user) {
|
||||
statistics.push({
|
||||
titleKey: "app",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(apps),
|
||||
path: "/manage/apps",
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
statistics.push({
|
||||
titleKey: "group",
|
||||
subtitleKey: "authorization",
|
||||
count: await db.$count(groups),
|
||||
path: "/manage/users/groups",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("search-engine-create")) {
|
||||
statistics.push({
|
||||
titleKey: "searchEngine",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(searchEngines),
|
||||
path: "/manage/search-engines",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("media-upload")) {
|
||||
statistics.push({
|
||||
titleKey: "media",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(
|
||||
medias,
|
||||
ctx.session.user.permissions.includes("media-view-all")
|
||||
? undefined
|
||||
: eq(medias.creatorId, ctx.session.user.id),
|
||||
),
|
||||
path: "/manage/medias",
|
||||
});
|
||||
}
|
||||
|
||||
return statistics;
|
||||
}),
|
||||
});
|
||||
|
||||
const getCountForTableAsync = async (db: Database, table: AnySQLiteTable, canView: boolean) => {
|
||||
if (!canView) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return await db.$count(table);
|
||||
};
|
||||
|
||||
@@ -2986,13 +2986,19 @@
|
||||
"invite": "Invites",
|
||||
"integration": "Integrations",
|
||||
"app": "Apps",
|
||||
"group": "Groups"
|
||||
"group": "Groups",
|
||||
"searchEngine": "Search engines",
|
||||
"media": "Medias"
|
||||
},
|
||||
"statisticLabel": {
|
||||
"boards": "Boards",
|
||||
"resources": "Resources",
|
||||
"authentication": "Authentication",
|
||||
"authorization": "Authorization"
|
||||
},
|
||||
"heroBanner": {
|
||||
"title": "Welcome back to your",
|
||||
"subtitle": "{app} Board"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -623,6 +623,9 @@ importers:
|
||||
'@homarr/server-settings':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../server-settings
|
||||
'@homarr/translation':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../translation
|
||||
'@homarr/validation':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../validation
|
||||
|
||||
Reference in New Issue
Block a user