mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 15:05:48 +01:00
✨ Add manage dashboards page
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
IconDashboard,
|
||||
IconGitFork,
|
||||
IconHome,
|
||||
IconLayoutDashboard,
|
||||
IconLogout,
|
||||
IconMailForward,
|
||||
IconQuestionMark,
|
||||
@@ -75,6 +76,16 @@ export const MainLayout = ({ children }: MainLayoutProps) => {
|
||||
component={Link}
|
||||
href="/manage/"
|
||||
/>
|
||||
<NavLink
|
||||
label="Boards"
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconLayoutDashboard size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
component={Link}
|
||||
href="/manage/boards"
|
||||
/>
|
||||
<NavLink
|
||||
label="Users"
|
||||
icon={
|
||||
|
||||
56
src/modals/create-dashboard/create-dashboard.modal.tsx
Normal file
56
src/modals/create-dashboard/create-dashboard.modal.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Button, Group, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { getStaticFallbackConfig } from '~/tools/config/getFallbackConfig';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { createDashboardSchemaValidation } from '~/validations/dashboards';
|
||||
|
||||
export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalProps<{}>) => {
|
||||
const apiContext = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.config.save.useMutation({
|
||||
onSuccess: async () => {
|
||||
await apiContext.config.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: '',
|
||||
},
|
||||
validate: i18nZodResolver(createDashboardSchemaValidation),
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>A name cannot be changed after a dashboard has been created.</Text>
|
||||
|
||||
<TextInput label="Name" withAsterisk {...form.getInputProps('name')} />
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const fallbackConfig = getStaticFallbackConfig(form.values.name);
|
||||
await mutateAsync({
|
||||
name: form.values.name,
|
||||
config: fallbackConfig,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/Cate
|
||||
import { DeleteUserModal } from './delete-user/delete-user.modal';
|
||||
import { CreateRegistrationTokenModal } from './create-registration-token/create-registration-token.modal';
|
||||
import { DeleteRegistrationTokenModal } from './delete-registration-token/delete-registration-token.modal';
|
||||
import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal';
|
||||
|
||||
export const modals = {
|
||||
editApp: EditAppModal,
|
||||
@@ -20,7 +21,8 @@ export const modals = {
|
||||
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
||||
deleteUserModal: DeleteUserModal,
|
||||
createRegistrationTokenModal: CreateRegistrationTokenModal,
|
||||
deleteRegistrationTokenModal: DeleteRegistrationTokenModal
|
||||
deleteRegistrationTokenModal: DeleteRegistrationTokenModal,
|
||||
createDashboardModal: CreateDashboardModal
|
||||
};
|
||||
|
||||
declare module '@mantine/modals' {
|
||||
|
||||
129
src/pages/manage/boards/index.tsx
Normal file
129
src/pages/manage/boards/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Menu,
|
||||
SimpleGrid,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { IconDotsVertical, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { MainLayout } from '~/components/layout/admin/main-admin.layout';
|
||||
import { CommonHeader } from '~/components/layout/common-header';
|
||||
import { sleep } from '~/tools/client/time';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
const BoardsPage = () => {
|
||||
const { data } = api.config.all.useQuery();
|
||||
const context = api.useContext();
|
||||
const { mutateAsync: deletionMutationAsync } = api.config.delete.useMutation({
|
||||
onSettled: () => {
|
||||
void context.config.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<CommonHeader>
|
||||
<title>Boards • Homarr</title>
|
||||
</CommonHeader>
|
||||
|
||||
<Title mb="xl">Boards</Title>
|
||||
|
||||
<Flex justify="end" mb="md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.openContextModal({
|
||||
modal: 'createDashboardModal',
|
||||
title: <Text>Create new dashboard</Text>,
|
||||
innerProps: {},
|
||||
});
|
||||
}}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
Create new dashboard
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{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)} />
|
||||
<Group position="apart" mb="xs">
|
||||
<Text weight={500}>{board}</Text>
|
||||
<Badge color="pink" variant="light">
|
||||
Filesystem
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Text size="sm" color="dimmed">
|
||||
With Fjord Tours you can explore more of the magical fjord landscapes with tours and
|
||||
activities on and around the fjords of Norway
|
||||
</Text>
|
||||
|
||||
<Group mt="md">
|
||||
<Button
|
||||
component={Link}
|
||||
style={{ flexGrow: 1 }}
|
||||
variant="default"
|
||||
color="blue"
|
||||
radius="md"
|
||||
href={`/board/${board}`}
|
||||
>
|
||||
View dashboard
|
||||
</Button>
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon h={34} w={34} variant="default">
|
||||
<IconDotsVertical size="1rem" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={async () => {
|
||||
append(board);
|
||||
// give user feedback, that it's being deleted
|
||||
await sleep(500);
|
||||
deletionMutationAsync({
|
||||
name: board,
|
||||
}).finally(async () => {
|
||||
await sleep(500);
|
||||
filter((item, _) => item !== board);
|
||||
});
|
||||
}}
|
||||
icon={<IconTrash size="1rem" />}
|
||||
color="red"
|
||||
>
|
||||
Permanently delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardsPage;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ConfigType } from '~/types/config';
|
||||
import defaultConfig from '../../../data/configs/default.json';
|
||||
|
||||
export const getFallbackConfig = (name?: string) => ({
|
||||
@@ -6,3 +7,64 @@ export const getFallbackConfig = (name?: string) => ({
|
||||
name: name ?? 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export const getStaticFallbackConfig = (name: string): ConfigType => ({
|
||||
schemaVersion: 1,
|
||||
configProperties: {
|
||||
name: name,
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: '47af36c0-47c1-4e5b-bfc7-ad645ee6a33f',
|
||||
position: 1,
|
||||
name: 'Welcome to Homarr 🎉',
|
||||
},
|
||||
],
|
||||
wrappers: [
|
||||
{
|
||||
id: 'default',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: '47af36c0-47c1-4e5b-bfc7-ad645ee6a326',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
apps: [],
|
||||
widgets: [],
|
||||
settings: {
|
||||
common: {
|
||||
searchEngine: {
|
||||
type: 'google',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
customization: {
|
||||
layout: {
|
||||
enabledLeftSidebar: false,
|
||||
enabledRightSidebar: false,
|
||||
enabledDocker: false,
|
||||
enabledPing: false,
|
||||
enabledSearchbar: true,
|
||||
},
|
||||
accessibility: {
|
||||
disablePingPulse: false,
|
||||
replacePingDotsWithIcons: false
|
||||
},
|
||||
pageTitle: 'Homarr ⭐️',
|
||||
logoImageUrl: '/imgs/logo/logo.png',
|
||||
faviconUrl: '/imgs/favicon/favicon-squared.png',
|
||||
backgroundImageUrl: '',
|
||||
customCss: '',
|
||||
colors: {
|
||||
primary: 'red',
|
||||
secondary: 'yellow',
|
||||
shade: 7,
|
||||
},
|
||||
appOpacity: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface SettingsType {
|
||||
|
||||
export interface CommonSettingsType {
|
||||
searchEngine: SearchEngineCommonSettingsType;
|
||||
defaultConfig: string;
|
||||
}
|
||||
|
||||
export type SearchEngineCommonSettingsType =
|
||||
|
||||
5
src/validations/dashboards.ts
Normal file
5
src/validations/dashboards.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createDashboardSchemaValidation = z.object({
|
||||
name: z.string().min(2).max(25),
|
||||
});
|
||||
Reference in New Issue
Block a user