mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
✨ Add manage dashboards page
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
IconDashboard,
|
IconDashboard,
|
||||||
IconGitFork,
|
IconGitFork,
|
||||||
IconHome,
|
IconHome,
|
||||||
|
IconLayoutDashboard,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMailForward,
|
IconMailForward,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
@@ -75,6 +76,16 @@ export const MainLayout = ({ children }: MainLayoutProps) => {
|
|||||||
component={Link}
|
component={Link}
|
||||||
href="/manage/"
|
href="/manage/"
|
||||||
/>
|
/>
|
||||||
|
<NavLink
|
||||||
|
label="Boards"
|
||||||
|
icon={
|
||||||
|
<ThemeIcon size="md" variant="light" color="red">
|
||||||
|
<IconLayoutDashboard size="1rem" />
|
||||||
|
</ThemeIcon>
|
||||||
|
}
|
||||||
|
component={Link}
|
||||||
|
href="/manage/boards"
|
||||||
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
label="Users"
|
label="Users"
|
||||||
icon={
|
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 { DeleteUserModal } from './delete-user/delete-user.modal';
|
||||||
import { CreateRegistrationTokenModal } from './create-registration-token/create-registration-token.modal';
|
import { CreateRegistrationTokenModal } from './create-registration-token/create-registration-token.modal';
|
||||||
import { DeleteRegistrationTokenModal } from './delete-registration-token/delete-registration-token.modal';
|
import { DeleteRegistrationTokenModal } from './delete-registration-token/delete-registration-token.modal';
|
||||||
|
import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal';
|
||||||
|
|
||||||
export const modals = {
|
export const modals = {
|
||||||
editApp: EditAppModal,
|
editApp: EditAppModal,
|
||||||
@@ -20,7 +21,8 @@ export const modals = {
|
|||||||
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
||||||
deleteUserModal: DeleteUserModal,
|
deleteUserModal: DeleteUserModal,
|
||||||
createRegistrationTokenModal: CreateRegistrationTokenModal,
|
createRegistrationTokenModal: CreateRegistrationTokenModal,
|
||||||
deleteRegistrationTokenModal: DeleteRegistrationTokenModal
|
deleteRegistrationTokenModal: DeleteRegistrationTokenModal,
|
||||||
|
createDashboardModal: CreateDashboardModal
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module '@mantine/modals' {
|
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';
|
import defaultConfig from '../../../data/configs/default.json';
|
||||||
|
|
||||||
export const getFallbackConfig = (name?: string) => ({
|
export const getFallbackConfig = (name?: string) => ({
|
||||||
@@ -6,3 +7,64 @@ export const getFallbackConfig = (name?: string) => ({
|
|||||||
name: name ?? 'default',
|
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 {
|
export interface CommonSettingsType {
|
||||||
searchEngine: SearchEngineCommonSettingsType;
|
searchEngine: SearchEngineCommonSettingsType;
|
||||||
defaultConfig: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchEngineCommonSettingsType =
|
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