mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-11 16:05:47 +01:00
Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
export const REPO_URL = 'ajnart/homarr';
|
export const REPO_URL = 'ajnart/homarr';
|
||||||
export const CURRENT_VERSION = 'v0.10.7';
|
export const CURRENT_VERSION = 'v0.11';
|
||||||
|
export const ICON_PICKER_SLICE_LIMIT = 36;
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -32,16 +32,16 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.0",
|
"@dnd-kit/utilities": "^3.2.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@mantine/carousel": "^5.9.0",
|
"@mantine/carousel": "^5.9.3",
|
||||||
"@mantine/core": "^5.9.0",
|
"@mantine/core": "^5.9.3",
|
||||||
"@mantine/dates": "^5.9.0",
|
"@mantine/dates": "^5.9.3",
|
||||||
"@mantine/dropzone": "^5.9.0",
|
"@mantine/dropzone": "^5.9.3",
|
||||||
"@mantine/form": "^5.9.0",
|
"@mantine/form": "^5.9.3",
|
||||||
"@mantine/hooks": "^5.9.0",
|
"@mantine/hooks": "^5.9.3",
|
||||||
"@mantine/modals": "^5.9.0",
|
"@mantine/modals": "^5.9.3",
|
||||||
"@mantine/next": "^5.9.0",
|
"@mantine/next": "^5.9.3",
|
||||||
"@mantine/notifications": "^5.9.0",
|
"@mantine/notifications": "^5.9.3",
|
||||||
"@mantine/prism": "^5.9.0",
|
"@mantine/prism": "^5.9.3",
|
||||||
"@nivo/core": "^0.79.0",
|
"@nivo/core": "^0.79.0",
|
||||||
"@nivo/line": "^0.79.1",
|
"@nivo/line": "^0.79.1",
|
||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tooltip": "The edit mode enables you to configure your dashboard",
|
||||||
|
"button": {
|
||||||
|
"disabled": "Enter Edit Mode",
|
||||||
|
"enabled": "Exit and Save"
|
||||||
|
},
|
||||||
|
"popover": {
|
||||||
|
"title": "Edit mode is enabled",
|
||||||
|
"text": "You can adjust and configure your apps now. Changes are not saved until you exit edit mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -18,7 +17,9 @@ import {
|
|||||||
IconVocabulary,
|
IconVocabulary,
|
||||||
IconWorldWww,
|
IconWorldWww,
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
|
import { InitOptions } from 'i18next';
|
||||||
import { i18n } from 'next-i18next';
|
import { i18n } from 'next-i18next';
|
||||||
|
import Image from 'next/image';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { CURRENT_VERSION } from '../../../data/constants';
|
import { CURRENT_VERSION } from '../../../data/constants';
|
||||||
import { usePrimaryGradient } from '../layout/useGradient';
|
import { usePrimaryGradient } from '../layout/useGradient';
|
||||||
@@ -118,13 +119,44 @@ interface InformationTableItem {
|
|||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExtendedInitOptions extends InitOptions {
|
||||||
|
locales: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const useInformationTableItems = (): InformationTableItem[] => {
|
const useInformationTableItems = (): InformationTableItem[] => {
|
||||||
const colorGradiant = usePrimaryGradient();
|
const colorGradiant = usePrimaryGradient();
|
||||||
|
|
||||||
const usedI18nNamespaces = i18n?.reportNamespaces?.getUsedNamespaces();
|
let items: InformationTableItem[] = [];
|
||||||
const configuredi18nLocales: string[] = i18n?.options.locales;
|
|
||||||
|
|
||||||
return [
|
if (i18n !== null) {
|
||||||
|
const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces();
|
||||||
|
const initOptions = i18n.options as ExtendedInitOptions;
|
||||||
|
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
{
|
||||||
|
icon: <IconLanguage size={20} />,
|
||||||
|
label: 'Loaded I18n translation namespaces',
|
||||||
|
content: (
|
||||||
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
|
{usedI18nNamespaces.length}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconVocabulary size={20} />,
|
||||||
|
label: 'Configured I18n locales',
|
||||||
|
content: (
|
||||||
|
<Badge variant="gradient" gradient={colorGradiant}>
|
||||||
|
{initOptions.locales.length}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
{
|
{
|
||||||
icon: <IconVersions size={20} />,
|
icon: <IconVersions size={20} />,
|
||||||
label: 'Homarr version',
|
label: 'Homarr version',
|
||||||
@@ -134,25 +166,9 @@ const useInformationTableItems = (): InformationTableItem[] => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: <IconLanguage size={20} />,
|
|
||||||
label: 'Loaded I18n translation namespaces',
|
|
||||||
content: (
|
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
|
||||||
{usedI18nNamespaces?.length ?? 'loading'}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconVocabulary size={20} />,
|
|
||||||
label: 'Configured I18n locales',
|
|
||||||
content: (
|
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
|
||||||
{configuredi18nLocales?.length ?? 'loading'}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
const useStyles = createStyles(() => ({
|
||||||
|
|||||||
@@ -1,460 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Anchor,
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
LoadingOverlay,
|
|
||||||
Modal,
|
|
||||||
PasswordInput,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
Tabs,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
|
||||||
import { IconApps } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { tryMatchPort, ServiceTypeList, Config } from '../../tools/types';
|
|
||||||
import apiKeyPaths from './apiKeyPaths.json';
|
|
||||||
import Tip from '../layout/Tip';
|
|
||||||
|
|
||||||
export function AddItemShelfButton(props: any) {
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
title={<Title order={3}>{t('modal.title')}</Title>}
|
|
||||||
opened={props.opened || opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
>
|
|
||||||
<AddAppShelfItemForm config={config} setConfig={setConfig} setOpened={setOpened} />
|
|
||||||
</Modal>
|
|
||||||
<Tooltip withinPortal label={t('actionIcon.tooltip')}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="default"
|
|
||||||
radius="md"
|
|
||||||
size="xl"
|
|
||||||
color="blue"
|
|
||||||
style={props.style}
|
|
||||||
onClick={() => setOpened(true)}
|
|
||||||
>
|
|
||||||
<IconApps />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchIcon(name: string | undefined, form: any) {
|
|
||||||
if (name === undefined || name === '') return null;
|
|
||||||
fetch(
|
|
||||||
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/^dash\.$/, 'dashdot')}.png`
|
|
||||||
).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
form.setFieldValue('icon', res.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchService(name: string, form: any) {
|
|
||||||
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
|
|
||||||
if (service) {
|
|
||||||
form.setFieldValue('type', service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_ICON = '/imgs/favicon/favicon.png';
|
|
||||||
|
|
||||||
interface AddAppShelfItemFormProps {
|
|
||||||
setOpened: (b: boolean) => void;
|
|
||||||
config: Config;
|
|
||||||
setConfig: (config: Config) => void;
|
|
||||||
// Any other props you want to pass to the form
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) {
|
|
||||||
const { setOpened, config, setConfig } = props;
|
|
||||||
// Only get config and setConfig from useCOnfig if they are not present in props
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
|
||||||
|
|
||||||
// Extract all the categories from the services in config
|
|
||||||
const InitialCategories = config.services.reduce((acc, cur) => {
|
|
||||||
if (cur.category && !acc.includes(cur.category)) {
|
|
||||||
acc.push(cur.category);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as string[]);
|
|
||||||
const [categories, setCategories] = useState<string[]>(InitialCategories);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
id: props.id ?? uuidv4(),
|
|
||||||
type: props.type ?? 'Other',
|
|
||||||
category: props.category ?? null,
|
|
||||||
name: props.name ?? '',
|
|
||||||
icon: props.icon ?? DEFAULT_ICON,
|
|
||||||
url: props.url ?? '',
|
|
||||||
apiKey: props.apiKey ?? undefined,
|
|
||||||
username: props.username ?? undefined,
|
|
||||||
password: props.password ?? undefined,
|
|
||||||
openedUrl: props.openedUrl ?? undefined,
|
|
||||||
ping: props.ping ?? true,
|
|
||||||
newTab: props.newTab ?? true,
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
apiKey: () => null,
|
|
||||||
// Validate icon with a regex
|
|
||||||
icon: (value: string) =>
|
|
||||||
// Disable matching to allow any values
|
|
||||||
null,
|
|
||||||
// Validate url with a regex http/https
|
|
||||||
url: (value: string) => {
|
|
||||||
try {
|
|
||||||
const _isValid = new URL(value);
|
|
||||||
} catch (e) {
|
|
||||||
return t('modal.form.validation.invalidUrl');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
form.values.name !== debounced ||
|
|
||||||
form.values.icon !== DEFAULT_ICON ||
|
|
||||||
form.values.type !== 'Other'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MatchIcon(form.values.name, form);
|
|
||||||
MatchService(form.values.name, form);
|
|
||||||
tryMatchPort(form.values.name, form);
|
|
||||||
}, [debounced]);
|
|
||||||
|
|
||||||
// Try to set const hostname to new URL(form.values.url).hostname)
|
|
||||||
// If it fails, set it to the form.values.url
|
|
||||||
let hostname = form.values.url;
|
|
||||||
try {
|
|
||||||
hostname = new URL(form.values.url).origin;
|
|
||||||
} catch (e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Center mb="lg">
|
|
||||||
<Image
|
|
||||||
height={120}
|
|
||||||
width={120}
|
|
||||||
fit="contain"
|
|
||||||
src={form.values.icon}
|
|
||||||
alt="Placeholder"
|
|
||||||
withPlaceholder
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit(() => {
|
|
||||||
const newForm = { ...form.values };
|
|
||||||
if (newForm.newTab === true) newForm.newTab = undefined;
|
|
||||||
if (newForm.openedUrl === '') newForm.openedUrl = undefined;
|
|
||||||
if (newForm.category === null) newForm.category = undefined;
|
|
||||||
if (newForm.ping === true) newForm.ping = undefined;
|
|
||||||
// If service already exists, update it.
|
|
||||||
if (config.services && config.services.find((s) => s.id === newForm.id)) {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
// replace the found item by matching ID
|
|
||||||
services: config.services.map((s) => {
|
|
||||||
if (s.id === newForm.id) {
|
|
||||||
return {
|
|
||||||
...newForm,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
services: [...config.services, newForm],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setOpened(false);
|
|
||||||
form.reset();
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="Options">
|
|
||||||
<Tabs.List grow>
|
|
||||||
<Tabs.Tab value="Options">{t('modal.tabs.options.title')}</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="Advanced Options">{t('modal.tabs.advancedOptions.title')}</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
<Tabs.Panel value="Options">
|
|
||||||
<Space h="sm" />
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.serviceName.label')}
|
|
||||||
placeholder={t('modal.tabs.options.form.serviceName.placeholder')}
|
|
||||||
{...form.getInputProps('name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.iconUrl.label')}
|
|
||||||
placeholder={DEFAULT_ICON}
|
|
||||||
{...form.getInputProps('icon')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.serviceUrl.label')}
|
|
||||||
placeholder="http://localhost:7575"
|
|
||||||
{...form.getInputProps('url')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.onClickUrl.label')}
|
|
||||||
placeholder="http://sonarr.example.com"
|
|
||||||
{...form.getInputProps('openedUrl')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('modal.tabs.options.form.serviceType.label')}
|
|
||||||
defaultValue={t('modal.tabs.options.form.serviceType.defaultValue')}
|
|
||||||
placeholder={t('modal.tabs.options.form.serviceType.placeholder')}
|
|
||||||
required
|
|
||||||
searchable
|
|
||||||
data={ServiceTypeList}
|
|
||||||
{...form.getInputProps('type')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('modal.tabs.options.form.category.label')}
|
|
||||||
data={categories}
|
|
||||||
placeholder={t('modal.tabs.options.form.category.placeholder')}
|
|
||||||
nothingFound={t('modal.tabs.options.form.category.nothingFound')}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
creatable
|
|
||||||
onCreate={(query) => {
|
|
||||||
const item = { value: query, label: query };
|
|
||||||
setCategories([...InitialCategories, query]);
|
|
||||||
return item;
|
|
||||||
}}
|
|
||||||
getCreateLabel={(query) =>
|
|
||||||
t('modal.tabs.options.form.category.createLabel', {
|
|
||||||
query,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{...form.getInputProps('category')}
|
|
||||||
/>
|
|
||||||
<LoadingOverlay visible={isLoading} />
|
|
||||||
{(form.values.type === 'Sonarr' ||
|
|
||||||
form.values.type === 'Radarr' ||
|
|
||||||
form.values.type === 'Lidarr' ||
|
|
||||||
form.values.type === 'Overseerr' ||
|
|
||||||
form.values.type === 'Jellyseerr' ||
|
|
||||||
form.values.type === 'Readarr' ||
|
|
||||||
form.values.type === 'Sabnzbd') && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={t('modal.tabs.options.form.integrations.apiKey.label')}
|
|
||||||
placeholder={t('modal.tabs.options.form.integrations.apiKey.placeholder')}
|
|
||||||
value={form.values.apiKey}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.apiKey &&
|
|
||||||
t('modal.tabs.options.form.integrations.apiKey.validation.noKey')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tip>
|
|
||||||
{t('modal.tabs.options.form.integrations.apiKey.tip.text')}{' '}
|
|
||||||
<Anchor
|
|
||||||
target="_blank"
|
|
||||||
weight="bold"
|
|
||||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
|
||||||
href={`${hostname}/${
|
|
||||||
apiKeyPaths[form.values.type as keyof typeof apiKeyPaths]
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t('modal.tabs.options.form.integrations.apiKey.tip.link')}
|
|
||||||
</Anchor>
|
|
||||||
</Tip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'qBittorrent' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.qBittorrent.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.qBittorrent.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'Deluge' && (
|
|
||||||
<>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.deluge.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.deluge.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'Transmission' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.transmission.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.transmission.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{form.values.type === 'NZBGet' && (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.nzbget.username.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.username.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.username}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('username', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.username &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.username.validation.invalidUsername'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={t('modal.tabs.options.form.integrations.nzbget.password.label')}
|
|
||||||
placeholder={t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.password.placeholder'
|
|
||||||
)}
|
|
||||||
value={form.values.password}
|
|
||||||
onChange={(event) => {
|
|
||||||
form.setFieldValue('password', event.currentTarget.value);
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
form.errors.password &&
|
|
||||||
t(
|
|
||||||
'modal.tabs.options.form.integrations.nzbget.password.validation.invalidPassword'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Tabs.Panel>
|
|
||||||
<Tabs.Panel value="Advanced Options">
|
|
||||||
<Space h="sm" />
|
|
||||||
<Stack>
|
|
||||||
<Switch
|
|
||||||
label="Ping service"
|
|
||||||
defaultChecked={form.values.ping}
|
|
||||||
{...form.getInputProps('ping')}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')}
|
|
||||||
defaultChecked={form.values.newTab}
|
|
||||||
{...form.getInputProps('newTab')}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
<Group grow position="center" mt="xl">
|
|
||||||
<Button type="submit">
|
|
||||||
{props.message ?? t('modal.tabs.advancedOptions.form.buttons.submit.content')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ import { useState } from 'react';
|
|||||||
import PingComponent from '../../modules/ping/PingModule';
|
import PingComponent from '../../modules/ping/PingModule';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { serviceItem } from '../../tools/types';
|
import { serviceItem } from '../../tools/types';
|
||||||
import AppShelfMenu from './AppShelfMenu';
|
import { TileMenu } from '../Dashboard/Menu/TileMenu';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
item: {
|
item: {
|
||||||
@@ -104,7 +104,7 @@ export function AppShelfItem(props: any) {
|
|||||||
opacity: hovering ? 1 : 0,
|
opacity: hovering ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppShelfMenu service={service} />
|
{ /* <TileMenu service={service} /> TODO: Remove this component */ }
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { ActionIcon, Menu, Modal, Text } from '@mantine/core';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconCheck as Check, IconEdit as Edit, IconMenu, IconTrash as Trash } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import { AddAppShelfItemForm } from './AddAppShelfItem';
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
|
||||||
|
|
||||||
export default function AppShelfMenu(props: any) {
|
|
||||||
const { service }: { service: serviceItem } = props;
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const { secondaryColor } = useColorTheme();
|
|
||||||
const { t } = useTranslation('layout/app-shelf-menu');
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
opened={props.opened || opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
title={t('modal.title')}
|
|
||||||
>
|
|
||||||
<AddAppShelfItemForm
|
|
||||||
config={config}
|
|
||||||
setConfig={setConfig}
|
|
||||||
setOpened={setOpened}
|
|
||||||
{...service}
|
|
||||||
message={t('modal.buttons.save')}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<Menu
|
|
||||||
withinPortal
|
|
||||||
width={150}
|
|
||||||
shadow="xl"
|
|
||||||
withArrow
|
|
||||||
radius="md"
|
|
||||||
position="right"
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
// Add shadow and elevation to the body
|
|
||||||
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.05)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon style={{}}>
|
|
||||||
<IconMenu />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
|
||||||
<Menu.Item color={secondaryColor} icon={<Edit />} onClick={() => setOpened(true)}>
|
|
||||||
{t('menu.actions.edit')}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
|
||||||
<Menu.Item
|
|
||||||
color="red"
|
|
||||||
onClick={(e: any) => {
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
services: config.services.filter((s) => s.id !== service.id),
|
|
||||||
});
|
|
||||||
showNotification({
|
|
||||||
autoClose: 5000,
|
|
||||||
title: (
|
|
||||||
<Text>
|
|
||||||
Service <b>{service.name}</b> removed successfully!
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
color: 'green',
|
|
||||||
icon: <Check />,
|
|
||||||
message: undefined,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={<Trash />}
|
|
||||||
>
|
|
||||||
{t('menu.actions.delete')}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
76
src/components/Dashboard/Menu/TileMenu.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { ActionIcon, Menu, Text } from '@mantine/core';
|
||||||
|
import { openContextModal } from '@mantine/modals';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconCheck, IconEdit, IconMenu, IconTrash } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useConfigContext } from '../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../config/store';
|
||||||
|
import { useColorTheme } from '../../../tools/color';
|
||||||
|
import { ServiceType } from '../../../types/service';
|
||||||
|
|
||||||
|
interface TileMenuProps {
|
||||||
|
service: ServiceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TileMenu = ({ service }: TileMenuProps) => {
|
||||||
|
const { secondaryColor } = useColorTheme();
|
||||||
|
const { t } = useTranslation('layout/app-shelf-menu');
|
||||||
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
const { name: configName } = useConfigContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu withinPortal width={150} shadow="xl" withArrow radius="md" position="right">
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon>
|
||||||
|
<IconMenu />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Label>{t('menu.labels.settings')}</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
color={secondaryColor}
|
||||||
|
icon={<IconEdit />}
|
||||||
|
onClick={() =>
|
||||||
|
openContextModal({
|
||||||
|
modal: 'changeTilePosition',
|
||||||
|
innerProps: {
|
||||||
|
type: 'service',
|
||||||
|
tile: service,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('menu.actions.edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Label>{t('menu.labels.dangerZone')}</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
color="red"
|
||||||
|
onClick={(e: any) => {
|
||||||
|
if (!configName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateConfig(configName, (previousConfig) => ({
|
||||||
|
...previousConfig,
|
||||||
|
services: previousConfig.services.filter((x) => x.id !== service.id),
|
||||||
|
})).then(() => {
|
||||||
|
showNotification({
|
||||||
|
autoClose: 5000,
|
||||||
|
title: (
|
||||||
|
<Text>
|
||||||
|
Service <b>{service.name}</b> removed successfully!
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
color: 'green',
|
||||||
|
icon: <IconCheck />,
|
||||||
|
message: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={<IconTrash />}
|
||||||
|
>
|
||||||
|
{t('menu.actions.delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Button, Flex, Grid, NumberInput } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { ServiceType } from '../../../../types/service';
|
||||||
|
import { TileBaseType } from '../../../../types/tile';
|
||||||
|
|
||||||
|
export const ChangePositionModal = ({
|
||||||
|
context,
|
||||||
|
id,
|
||||||
|
innerProps,
|
||||||
|
}: ContextModalProps<{ type: 'service' | 'type'; tile: TileBaseType }>) => {
|
||||||
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
const { name: configName } = useConfigContext();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
tile: innerProps.tile,
|
||||||
|
},
|
||||||
|
validateInputOnChange: true,
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (!configName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileAsService = form.values.tile as ServiceType;
|
||||||
|
|
||||||
|
updateConfig(configName, (previous) => ({
|
||||||
|
...previous,
|
||||||
|
services: [...previous.services.filter((x) => x.id === tileAsService.id), tileAsService],
|
||||||
|
}));
|
||||||
|
|
||||||
|
closeModal(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput
|
||||||
|
max={99}
|
||||||
|
min={0}
|
||||||
|
label="X Position"
|
||||||
|
description="0 or higher"
|
||||||
|
{...form.getInputProps('tile.shape.location.x')}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput
|
||||||
|
max={99}
|
||||||
|
min={0}
|
||||||
|
label="Y Position"
|
||||||
|
description="0 or higher"
|
||||||
|
{...form.getInputProps('tile.shape.location.y')}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput
|
||||||
|
max={24}
|
||||||
|
min={1}
|
||||||
|
label="Width"
|
||||||
|
description="Between 1 and 24"
|
||||||
|
{...form.getInputProps('tile.shape.size.width')}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col xs={12} md={6}>
|
||||||
|
<NumberInput
|
||||||
|
max={24}
|
||||||
|
min={1}
|
||||||
|
label="Height"
|
||||||
|
description="Between 1 and 24"
|
||||||
|
{...form.getInputProps('tile.shape.size.height')}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Flex justify="end" gap="sm" mt="md">
|
||||||
|
<Button onClick={() => closeModal(id)} variant="light" color="gray">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Change Position</Button>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,24 +1,30 @@
|
|||||||
import Image from 'next/image';
|
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
|
||||||
import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
IconAccessPoint,
|
IconAccessPoint,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
|
IconAlertTriangle,
|
||||||
IconBrush,
|
IconBrush,
|
||||||
IconClick,
|
IconClick,
|
||||||
IconDeviceFloppy,
|
|
||||||
IconDoorExit,
|
|
||||||
IconPlug,
|
IconPlug,
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
import Image from 'next/image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { ServiceType } from '../../../../types/service';
|
import { ServiceType } from '../../../../types/service';
|
||||||
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
|
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
|
||||||
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
|
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
|
||||||
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
|
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
|
||||||
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
|
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
|
||||||
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
|
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
|
||||||
|
import { EditServiceModalTab } from './Tabs/type';
|
||||||
|
|
||||||
|
const serviceUrlRegex =
|
||||||
|
'(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})';
|
||||||
|
|
||||||
export const EditServiceModal = ({
|
export const EditServiceModal = ({
|
||||||
context,
|
context,
|
||||||
@@ -27,16 +33,66 @@ export const EditServiceModal = ({
|
|||||||
}: ContextModalProps<{ service: ServiceType }>) => {
|
}: ContextModalProps<{ service: ServiceType }>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
const { name: configName, config } = useConfigContext();
|
||||||
|
const updateConfig = useConfigStore((store) => store.updateConfig);
|
||||||
|
|
||||||
const form = useForm<ServiceType>({
|
const form = useForm<ServiceType>({
|
||||||
initialValues: innerProps.service,
|
initialValues: innerProps.service,
|
||||||
|
validate: {
|
||||||
|
name: (name) => (!name ? 'Name is required' : null),
|
||||||
|
url: (url) => {
|
||||||
|
if (!url) {
|
||||||
|
return 'Url is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.match(serviceUrlRegex)) {
|
||||||
|
return 'Value is not a valid url';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
appearance: {
|
||||||
|
iconUrl: (url: string) => {
|
||||||
|
if (url.length < 1) {
|
||||||
|
return 'This field is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviour: {
|
||||||
|
onClickUrl: (url: string) => {
|
||||||
|
if (url === undefined || url.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.match(serviceUrlRegex)) {
|
||||||
|
return 'Uri override is not a valid uri';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateInputOnChange: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (values: ServiceType) => {
|
const onSubmit = (values: ServiceType) => {
|
||||||
console.log('form submitted');
|
if (!configName) {
|
||||||
console.log(values);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(configName, (previousConfig) => ({
|
||||||
|
...previousConfig,
|
||||||
|
services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// also close the parent modal
|
||||||
|
context.closeAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general');
|
||||||
|
|
||||||
const tryCloseModal = () => {
|
const tryCloseModal = () => {
|
||||||
if (form.isDirty()) {
|
if (form.isDirty()) {
|
||||||
showNotification({
|
showNotification({
|
||||||
@@ -63,8 +119,27 @@ export const EditServiceModal = ({
|
|||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validationErrors = Object.keys(form.errors);
|
||||||
|
|
||||||
|
const ValidationErrorIndicator = ({ keys }: { keys: string[] }) => {
|
||||||
|
const relevantErrors = validationErrors.filter((x) => keys.includes(x));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeIcon opacity={relevantErrors.length === 0 ? 0 : 1} color="red" variant="light">
|
||||||
|
<IconAlertTriangle size={15} />
|
||||||
|
</ThemeIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{configName === undefined ||
|
||||||
|
(config === undefined && (
|
||||||
|
<Alert color="red">
|
||||||
|
There was an unexpected problem loading the configuration. Functionality might be
|
||||||
|
restricted. Please report this incident.
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
<Stack spacing={0} align="center" my="lg">
|
<Stack spacing={0} align="center" my="lg">
|
||||||
{form.values.appearance.iconUrl ? (
|
{form.values.appearance.iconUrl ? (
|
||||||
// disabled because image target is too dynamic for next image cache
|
// disabled because image target is too dynamic for next image cache
|
||||||
@@ -84,27 +159,52 @@ export const EditServiceModal = ({
|
|||||||
{form.values.name ?? 'New Service'}
|
{form.values.name ?? 'New Service'}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<Tabs defaultValue="general">
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onTabChange={(tab) => setActiveTab(tab as EditServiceModalTab)}
|
||||||
|
defaultValue="general"
|
||||||
|
>
|
||||||
<Tabs.List grow>
|
<Tabs.List grow>
|
||||||
<Tabs.Tab value="general" icon={<IconAdjustments size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
|
||||||
|
icon={<IconAdjustments size={14} />}
|
||||||
|
value="general"
|
||||||
|
>
|
||||||
General
|
General
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="behaviour" icon={<IconClick size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
|
||||||
|
icon={<IconClick size={14} />}
|
||||||
|
value="behaviour"
|
||||||
|
>
|
||||||
Behaviour
|
Behaviour
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="network" icon={<IconAccessPoint size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||||
|
icon={<IconAccessPoint size={14} />}
|
||||||
|
value="network"
|
||||||
|
>
|
||||||
Network
|
Network
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="appearance" icon={<IconBrush size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
|
||||||
|
icon={<IconBrush size={14} />}
|
||||||
|
value="appearance"
|
||||||
|
>
|
||||||
Appearance
|
Appearance
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="integration" icon={<IconPlug size={14} />}>
|
<Tabs.Tab
|
||||||
|
rightSection={<ValidationErrorIndicator keys={[]} />}
|
||||||
|
icon={<IconPlug size={14} />}
|
||||||
|
value="integration"
|
||||||
|
>
|
||||||
Integration
|
Integration
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<GeneralTab form={form} />
|
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
|
||||||
<BehaviourTab form={form} />
|
<BehaviourTab form={form} />
|
||||||
<NetworkTab form={form} />
|
<NetworkTab form={form} />
|
||||||
<AppearanceTab form={form} />
|
<AppearanceTab form={form} />
|
||||||
@@ -112,16 +212,10 @@ export const EditServiceModal = ({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Group position="right" mt={100}>
|
<Group position="right" mt={100}>
|
||||||
<Button
|
<Button onClick={tryCloseModal} px={50} variant="light" color="gray">
|
||||||
leftIcon={<IconDoorExit size={20} />}
|
|
||||||
px={50}
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
onClick={tryCloseModal}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<IconDeviceFloppy size={20} />} px={50}>
|
<Button disabled={!form.isValid()} px={50} type="submit">
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Tabs, TextInput, createStyles } from '@mantine/core';
|
import Image from 'next/image';
|
||||||
|
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconPhoto } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { ServiceType } from '../../../../../../types/service';
|
||||||
|
import { IconSelector } from './IconSelector/IconSelector';
|
||||||
|
|
||||||
interface AppearanceTabProps {
|
interface AppearanceTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||||
@@ -12,32 +13,56 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
|
|||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
const Image = () => {
|
const PreviewImage = () => {
|
||||||
if (form.values.appearance.iconUrl !== undefined) {
|
if (form.values.appearance.iconUrl !== undefined && form.values.appearance.iconUrl.length > 0) {
|
||||||
// disabled due to too many dynamic targets for next image cache
|
// disabled due to too many dynamic targets for next image cache
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="jife" />;
|
return <img className={classes.iconImage} src={form.values.appearance.iconUrl} alt="" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <IconPhoto />;
|
return (
|
||||||
|
<Image
|
||||||
|
src="/imgs/logo/logo.png"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
objectFit="contain"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Panel value="appearance" pt="lg">
|
<Tabs.Panel value="appearance" pt="lg">
|
||||||
|
<Flex gap={5}>
|
||||||
<TextInput
|
<TextInput
|
||||||
icon={<Image />}
|
|
||||||
label="Service Icon"
|
|
||||||
variant="default"
|
|
||||||
defaultValue={form.values.appearance.iconUrl}
|
defaultValue={form.values.appearance.iconUrl}
|
||||||
{...form.getInputProps('appearance.iconUrl')}
|
className={classes.textInput}
|
||||||
|
icon={<PreviewImage />}
|
||||||
|
label="Service Icon"
|
||||||
|
description="Logo of your service displayed in your dashboard. Must return a body content containg an image"
|
||||||
|
variant="default"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
required
|
required
|
||||||
|
{...form.getInputProps('appearance.iconUrl')}
|
||||||
/>
|
/>
|
||||||
|
<IconSelector
|
||||||
|
onChange={(item) =>
|
||||||
|
form.setValues({
|
||||||
|
appearance: {
|
||||||
|
iconUrl: item.url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
const useStyles = createStyles(() => ({
|
||||||
|
textInput: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
iconImage: {
|
iconImage: {
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
width: 20,
|
width: 20,
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
createStyles,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Loader,
|
||||||
|
Popover,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconSearch, IconX } from '@tabler/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants';
|
||||||
|
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
|
||||||
|
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
|
||||||
|
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
|
||||||
|
|
||||||
|
interface IconSelectorProps {
|
||||||
|
onChange: (icon: IconSelectorItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconSelector = ({ onChange }: IconSelectorProps) => {
|
||||||
|
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
|
||||||
|
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
|
||||||
|
converter: (item) => ({
|
||||||
|
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceCharacters = (value: string) =>
|
||||||
|
value.toLowerCase().replaceAll(' ', '').replaceAll('-', '');
|
||||||
|
|
||||||
|
const filteredItems = searchTerm
|
||||||
|
? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm)))
|
||||||
|
: data;
|
||||||
|
const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT);
|
||||||
|
const isTruncated =
|
||||||
|
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover width={310}>
|
||||||
|
<Popover.Target>
|
||||||
|
<Button
|
||||||
|
className={classes.actionIcon}
|
||||||
|
variant="default"
|
||||||
|
leftIcon={<IconSearch size={20} />}
|
||||||
|
>
|
||||||
|
Icon Picker
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Stack pt={4}>
|
||||||
|
<TextInput
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(event) => setSearchTerm(event.currentTarget.value)}
|
||||||
|
placeholder="Search for icons..."
|
||||||
|
variant="filled"
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon onClick={() => setSearchTerm('')}>
|
||||||
|
<IconX opacity={0.5} size={20} strokeWidth={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollArea style={{ height: 250 }} type="always">
|
||||||
|
<Flex gap={4} wrap="wrap" pr={15}>
|
||||||
|
{slicedFilteredItems.map((item) => (
|
||||||
|
<ActionIcon onClick={() => onChange(item)} size={40} p={3}>
|
||||||
|
<img className={classes.icon} src={item.url} alt="" />
|
||||||
|
</ActionIcon>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{isTruncated && (
|
||||||
|
<Stack spacing="xs" pr={15}>
|
||||||
|
<Divider mt={35} mx="xl" />
|
||||||
|
<Title order={6} color="dimmed" align="center">
|
||||||
|
Search is limited to {ICON_PICKER_SLICE_LIMIT} icons
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" align="center" size="sm">
|
||||||
|
To keep things snappy and fast, the search is limited to {ICON_PICKER_SLICE_LIMIT}{' '}
|
||||||
|
icons. Use the search box to find more icons.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles(() => ({
|
||||||
|
flameIcon: {
|
||||||
|
margin: '0 auto',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
},
|
||||||
|
actionIcon: {
|
||||||
|
alignSelf: 'end',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -15,32 +15,16 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconClick size={16} />}
|
icon={<IconClick size={16} />}
|
||||||
label="On click url"
|
label="On click url"
|
||||||
placeholder="Override the default service url when clicking on the service"
|
description="Overrides the service URL when clicking on the service"
|
||||||
|
placeholder="URL that should be opened instead when clicking on the service"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
mb="md"
|
||||||
{...form.getInputProps('onClickUrl')}
|
{...form.getInputProps('behaviour.onClickUrl')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
value="disable_handle"
|
label="Open in new tab"
|
||||||
label="Disable direct moving in edit modus"
|
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
|
||||||
description={
|
|
||||||
<Text color="dimmed" size="sm">
|
|
||||||
Disables the direct movement of the tile
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
mb="md"
|
|
||||||
{...form.getInputProps('isEditModeMovingDisabled')}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
value="freze"
|
|
||||||
label="Freeze tile within edit modus"
|
|
||||||
description={
|
|
||||||
<Text color="dimmed" size="sm">
|
|
||||||
Disables the movement of the tile when moving others
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
{...form.getInputProps('isEditModeTileFreezed')}
|
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +1,52 @@
|
|||||||
import { Tabs, TextInput } from '@mantine/core';
|
import { Group, Tabs, Text, TextInput } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconCursorText, IconLink } from '@tabler/icons';
|
import { IconCursorText, IconLink } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ServiceType } from '../../../../../../types/service';
|
import { ServiceType } from '../../../../../../types/service';
|
||||||
|
import { EditServiceModalTab } from '../type';
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>;
|
||||||
|
openTab: (tab: EditServiceModalTab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GeneralTab = ({ form }: GeneralTabProps) => {
|
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
return (
|
return (
|
||||||
<Tabs.Panel value="general" pt="lg">
|
<Tabs.Panel value="general" pt="lg">
|
||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconCursorText size={16} />}
|
icon={<IconCursorText size={16} />}
|
||||||
label="Service name"
|
label="Service name"
|
||||||
|
description="Used for displaying the service on the dashboard"
|
||||||
placeholder="My example service"
|
placeholder="My example service"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
mb="md"
|
||||||
required
|
withAsterisk
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
icon={<IconLink size={16} />}
|
icon={<IconLink size={16} />}
|
||||||
label="Service url"
|
label="Service url"
|
||||||
|
description={
|
||||||
|
<Text>
|
||||||
|
URL that will be opened when clicking on the service. Can be overwritten using
|
||||||
|
<Text
|
||||||
|
onClick={() => openTab('behaviour')}
|
||||||
|
variant="link"
|
||||||
|
span
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
on click URL{' '}
|
||||||
|
</Text>
|
||||||
|
when using external URLs to enhance security.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
placeholder="https://google.com"
|
placeholder="https://google.com"
|
||||||
variant="default"
|
variant="default"
|
||||||
required
|
withAsterisk
|
||||||
{...form.getInputProps('url')}
|
{...form.getInputProps('url')}
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {
|
import { Group, Select, SelectItem, Text } from '@mantine/core';
|
||||||
Alert,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
PasswordInput,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Space,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { IconKey, IconUser } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { forwardRef, useState } from 'react';
|
import { forwardRef, useState } from 'react';
|
||||||
import { ServiceType } from '../../../../../../../../../types/service';
|
import { ServiceType } from '../../../../../../../../types/service';
|
||||||
import { TextExplanation } from '../TextExplanation/TextExplanation';
|
|
||||||
|
|
||||||
interface IntegrationSelectorProps {
|
interface IntegrationSelectorProps {
|
||||||
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
|
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>;
|
||||||
@@ -62,11 +50,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextExplanation />
|
|
||||||
<Space h="sm" />
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Configure this service for the following integration"
|
label="Integration configuration"
|
||||||
|
description="Treats this service as the selected integration and provides you with per-service configuration"
|
||||||
placeholder="Select your desired configuration"
|
placeholder="Select your desired configuration"
|
||||||
itemComponent={SelectItemComponent}
|
itemComponent={SelectItemComponent}
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Text } from '@mantine/core';
|
|
||||||
|
|
||||||
export const TextExplanation = () => {
|
|
||||||
return (
|
|
||||||
<Text color="dimmed">
|
|
||||||
You can optionally connect your services using integrations. Integration elements on your
|
|
||||||
dashboard will communicate to your services using the configuration below.
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -14,6 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
|||||||
<Tabs.Panel value="network" pt="lg">
|
<Tabs.Panel value="network" pt="lg">
|
||||||
<Switch
|
<Switch
|
||||||
label="Enable status checker"
|
label="Enable status checker"
|
||||||
|
description="Sends a simple HTTP / HTTPS request to check if your service is online"
|
||||||
mb="md"
|
mb="md"
|
||||||
defaultChecked={form.values.network.enabledStatusChecker}
|
defaultChecked={form.values.network.enabledStatusChecker}
|
||||||
{...form.getInputProps('network.enabledStatusChecker')}
|
{...form.getInputProps('network.enabledStatusChecker')}
|
||||||
@@ -22,6 +23,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
required
|
required
|
||||||
label="HTTP status codes"
|
label="HTTP status codes"
|
||||||
|
description="Determines what response codes are allowed for this service to be 'Online'"
|
||||||
data={StatusCodes}
|
data={StatusCodes}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
|
|||||||
1
src/components/Dashboard/Modals/EditService/Tabs/type.ts
Normal file
1
src/components/Dashboard/Modals/EditService/Tabs/type.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type EditServiceModalTab = 'general' | 'behaviour' | 'network' | 'appereance' | 'integration';
|
||||||
@@ -42,10 +42,14 @@ export const AvailableElementTypes = ({
|
|||||||
okStatus: [],
|
okStatus: [],
|
||||||
},
|
},
|
||||||
behaviour: {
|
behaviour: {
|
||||||
isOpeningNewTab: false,
|
isOpeningNewTab: true,
|
||||||
|
onClickUrl: '',
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: 'wrapper',
|
type: 'sidebar',
|
||||||
|
properties: {
|
||||||
|
location: 'right',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ActionIcon, Button, Group, Text } from '@mantine/core';
|
import { Button, Text } from '@mantine/core';
|
||||||
import { IconArrowNarrowLeft } from '@tabler/icons';
|
import { IconArrowNarrowLeft } from '@tabler/icons';
|
||||||
|
|
||||||
interface SelectorBackArrowProps {
|
interface SelectorBackArrowProps {
|
||||||
onClickBack: () => void;
|
onClickBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps) => {
|
export const SelectorBackArrow = ({ onClickBack }: SelectorBackArrowProps) => (
|
||||||
return (
|
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<IconArrowNarrowLeft />}
|
leftIcon={<IconArrowNarrowLeft />}
|
||||||
onClick={onClickBack}
|
onClick={onClickBack}
|
||||||
@@ -18,4 +17,3 @@ export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps
|
|||||||
<Text>See all available elements</Text>
|
<Text>See all available elements</Text>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Space, Stack, Text } from '@mantine/core';
|
import { Space, Stack, Text } from '@mantine/core';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import ConfigChanger from '../Config/ConfigChanger';
|
import ConfigChanger from '../../Config/ConfigChanger';
|
||||||
import ConfigActions from './Common/ConfigActions';
|
import ConfigActions from './Config/ConfigActions';
|
||||||
import LanguageSelect from './Common/LanguageSelect';
|
import LanguageSelect from './Language/LanguageSelect';
|
||||||
import { SearchEngineSelector } from './Common/SearchEngineSelector';
|
import { SearchEngineSelector } from './SearchEngine/SearchEngineSelector';
|
||||||
import { SearchNewTabSwitch } from './Common/SearchNewTabSwitch';
|
import { SearchNewTabSwitch } from './SearchNewTabSwitch';
|
||||||
|
|
||||||
export default function CommonSettings() {
|
export default function CommonSettings() {
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button, Center, Group } from '@mantine/core';
|
import { ActionIcon, Center, createStyles, Flex, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { IconCheck, IconDownload, IconPlus, IconTrash, IconX } from '@tabler/icons';
|
import { IconCheck, IconCopy, IconDownload, IconTrash, IconX } from '@tabler/icons';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import fileDownload from 'js-file-download';
|
import fileDownload from 'js-file-download';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import Tip from '../../layout/Tip';
|
import Tip from '../../../layout/Tip';
|
||||||
import { CreateConfigCopyModal } from './ConfigActions/CreateCopyModal';
|
import { CreateConfigCopyModal } from './CreateCopyModal';
|
||||||
|
|
||||||
export default function ConfigActions() {
|
export default function ConfigActions() {
|
||||||
const { t } = useTranslation(['settings/general/config-changer', 'settings/common']);
|
const { t } = useTranslation(['settings/general/config-changer', 'settings/common']);
|
||||||
@@ -26,6 +26,9 @@ export default function ConfigActions() {
|
|||||||
await mutateAsync();
|
await mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const { colors } = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateConfigCopyModal
|
<CreateConfigCopyModal
|
||||||
@@ -33,32 +36,25 @@ export default function ConfigActions() {
|
|||||||
closeModal={createCopyModal.close}
|
closeModal={createCopyModal.close}
|
||||||
initialConfigName={config.configProperties.name}
|
initialConfigName={config.configProperties.name}
|
||||||
/>
|
/>
|
||||||
<Group spacing="xs" position="center">
|
<Flex gap="xs" justify="stretch">
|
||||||
<Button
|
<ActionIcon className={classes.actionIcon} onClick={handleDownload} variant="default">
|
||||||
size="xs"
|
<IconDownload size={20} />
|
||||||
leftIcon={<IconDownload size={18} />}
|
<Text>{t('buttons.download')}</Text>
|
||||||
variant="default"
|
</ActionIcon>
|
||||||
onClick={handleDownload}
|
<ActionIcon
|
||||||
>
|
className={classes.actionIcon}
|
||||||
{t('buttons.download')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<IconTrash size={18} />}
|
|
||||||
variant="default"
|
|
||||||
onClick={handleDeletion}
|
onClick={handleDeletion}
|
||||||
|
color="red"
|
||||||
|
variant="light"
|
||||||
>
|
>
|
||||||
{t('buttons.delete.text')}
|
<IconTrash color={colors.red[2]} size={20} />
|
||||||
</Button>
|
<Text>{t('buttons.delete.text')}</Text>
|
||||||
<Button
|
</ActionIcon>
|
||||||
size="xs"
|
<ActionIcon className={classes.actionIcon} onClick={createCopyModal.open} variant="default">
|
||||||
leftIcon={<IconPlus size={18} />}
|
<IconCopy size={20} />
|
||||||
variant="default"
|
<Text>{t('buttons.saveCopy')}</Text>
|
||||||
onClick={createCopyModal.open}
|
</ActionIcon>
|
||||||
>
|
</Flex>
|
||||||
{t('buttons.saveCopy')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
||||||
@@ -67,6 +63,23 @@ export default function ConfigActions() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles(() => ({
|
||||||
|
actionIcon: {
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
maxWidth: 'auto',
|
||||||
|
maxHeight: 'auto',
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
rowGap: 10,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const useDeleteConfigMutation = (configName: string) => {
|
const useDeleteConfigMutation = (configName: string) => {
|
||||||
const { t } = useTranslation(['settings/general/config-changer']);
|
const { t } = useTranslation(['settings/general/config-changer']);
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
|
|||||||
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
|
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import { CURRENT_VERSION } from '../../../data/constants';
|
import { CURRENT_VERSION } from '../../../../data/constants';
|
||||||
|
|
||||||
export default function Credits() {
|
export default function Credits() {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
@@ -5,7 +5,7 @@ import { forwardRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { getCookie, setCookie } from 'cookies-next';
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
import { getLanguageByCode, Language } from '../../../tools/language';
|
import { getLanguageByCode, Language } from '../../../../tools/language';
|
||||||
|
|
||||||
export default function LanguageSelect() {
|
export default function LanguageSelect() {
|
||||||
const { t, i18n } = useTranslation('settings/general/internationalization');
|
const { t, i18n } = useTranslation('settings/general/internationalization');
|
||||||
@@ -2,13 +2,12 @@ import { Alert, Paper, SegmentedControl, Space, Stack, TextInput, Title } from '
|
|||||||
import { IconInfoCircle } from '@tabler/icons';
|
import { IconInfoCircle } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import {
|
import {
|
||||||
CommonSearchEngineCommonSettingsType,
|
CommonSearchEngineCommonSettingsType,
|
||||||
SearchEngineCommonSettingsType,
|
SearchEngineCommonSettingsType,
|
||||||
} from '../../../types/settings';
|
} from '../../../../types/settings';
|
||||||
import Tip from '../../layout/Tip';
|
|
||||||
import { SearchNewTabSwitch } from './SearchNewTabSwitch';
|
import { SearchNewTabSwitch } from './SearchNewTabSwitch';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Switch } from '@mantine/core';
|
import { Switch } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { SearchEngineCommonSettingsType } from '../../../types/settings';
|
import { SearchEngineCommonSettingsType } from '../../../../types/settings';
|
||||||
|
|
||||||
interface SearchNewTabSwitchProps {
|
interface SearchNewTabSwitchProps {
|
||||||
defaultValue: boolean | undefined;
|
defaultValue: boolean | undefined;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Stack } from '@mantine/core';
|
import { Stack } from '@mantine/core';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { ColorSelector } from './Customization/ColorSelector';
|
import { ColorSelector } from './Theme/ColorSelector';
|
||||||
import { BackgroundChanger } from './Customization/BackgroundChanger';
|
import { BackgroundChanger } from './Meta/BackgroundChanger';
|
||||||
import { CustomCssChanger } from './Customization/CustomCssChanger';
|
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
||||||
import { FaviconChanger } from './Customization/FaviconChanger';
|
import { FaviconChanger } from './Meta/FaviconChanger';
|
||||||
import { LogoImageChanger } from './Customization/LogoImageChanger';
|
import { LogoImageChanger } from './Meta/LogoImageChanger';
|
||||||
import { MetaTitleChanger } from './Customization/MetaTitleChanger';
|
import { MetaTitleChanger } from './Meta/MetaTitleChanger';
|
||||||
import { PageTitleChanger } from './Customization/PageTitleChanger';
|
import { PageTitleChanger } from './Meta/PageTitleChanger';
|
||||||
import { OpacitySelector } from './Customization/OpacitySelector';
|
import { OpacitySelector } from './Theme/OpacitySelector';
|
||||||
import { ShadeSelector } from './Customization/ShadeSelector';
|
import { ShadeSelector } from './Theme/ShadeSelector';
|
||||||
import { LayoutSelector } from './Customization/LayoutSelector';
|
import { LayoutSelector } from './Layout/LayoutSelector';
|
||||||
|
|
||||||
export default function CustomizationSettings() {
|
export default function CustomizationSettings() {
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
167
src/components/Settings/Customization/Layout/LayoutSelector.tsx
Normal file
167
src/components/Settings/Customization/Layout/LayoutSelector.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Center,
|
||||||
|
Checkbox,
|
||||||
|
createStyles,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { CustomizationSettingsType } from '../../../../types/settings';
|
||||||
|
import { Logo } from '../../../layout/Logo';
|
||||||
|
|
||||||
|
interface LayoutSelectorProps {
|
||||||
|
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add translations
|
||||||
|
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
const { name: configName } = useConfigContext();
|
||||||
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
|
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
||||||
|
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
||||||
|
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
||||||
|
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
||||||
|
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
||||||
|
|
||||||
|
const { colors, colorScheme } = useMantineTheme();
|
||||||
|
|
||||||
|
if (!configName) return null;
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
key: keyof CustomizationSettingsType['layout'],
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
setState: Dispatch<SetStateAction<boolean>>
|
||||||
|
) => {
|
||||||
|
const value = event.target.checked;
|
||||||
|
setState(value);
|
||||||
|
updateConfig(configName, (prev) => {
|
||||||
|
const { layout } = prev.settings.customization;
|
||||||
|
|
||||||
|
layout[key] = value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
settings: {
|
||||||
|
...prev.settings,
|
||||||
|
customization: {
|
||||||
|
...prev.settings.customization,
|
||||||
|
layout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Title order={6}>Dashboard layout</Title>
|
||||||
|
|
||||||
|
<Paper px="xs" py={4} withBorder>
|
||||||
|
<Group position="apart">
|
||||||
|
<Logo size="xs" />
|
||||||
|
<Group spacing={5}>
|
||||||
|
{searchBar ? (
|
||||||
|
<Paper
|
||||||
|
style={{
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: colorScheme === 'dark' ? colors.gray[8] : colors.gray[1],
|
||||||
|
}}
|
||||||
|
p={2}
|
||||||
|
w={60}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{docker ? <ActionIcon size={10} disabled /> : null}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Group align="stretch">
|
||||||
|
{leftSidebar && (
|
||||||
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
|
<Flex align="center" justify="center" direction="column">
|
||||||
|
<Text align="center">Sidebar</Text>
|
||||||
|
<Text color="dimmed" size="xs" align="center">
|
||||||
|
Only for
|
||||||
|
<br />
|
||||||
|
services &<br />
|
||||||
|
integrations
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||||
|
<Text align="center">Main</Text>
|
||||||
|
<Text color="dimmed" size="xs" align="center">
|
||||||
|
Cannot be turned of.
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{rightSidebar && (
|
||||||
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
|
<Flex align="center" justify="center" direction="column">
|
||||||
|
<Text align="center">Sidebar</Text>
|
||||||
|
<Text color="dimmed" size="xs" align="center">
|
||||||
|
Only for
|
||||||
|
<br />
|
||||||
|
services &<br />
|
||||||
|
integrations
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Checkbox
|
||||||
|
label="Enable left sidebar"
|
||||||
|
description="Optional. Can be used for services and integrations only"
|
||||||
|
checked={leftSidebar}
|
||||||
|
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable right sidebar"
|
||||||
|
description="Optional. Can be used for services and integrations only"
|
||||||
|
checked={rightSidebar}
|
||||||
|
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable search bar"
|
||||||
|
checked={searchBar}
|
||||||
|
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable docker"
|
||||||
|
checked={docker}
|
||||||
|
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable pings"
|
||||||
|
checked={ping}
|
||||||
|
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
primaryWrapper: {
|
||||||
|
flexGrow: 2,
|
||||||
|
},
|
||||||
|
secondaryWrapper: {
|
||||||
|
flexGrow: 1,
|
||||||
|
maxWidth: 100,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Center,
|
|
||||||
Checkbox,
|
|
||||||
createStyles,
|
|
||||||
Group,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconBrandDocker, IconLayout, IconSearch } from '@tabler/icons';
|
|
||||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../config/store';
|
|
||||||
import { CustomizationSettingsType } from '../../../types/settings';
|
|
||||||
import { Logo } from '../../layout/Logo';
|
|
||||||
|
|
||||||
interface LayoutSelectorProps {
|
|
||||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add translations
|
|
||||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
|
|
||||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
|
||||||
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
|
||||||
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
|
||||||
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
|
||||||
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
key: keyof CustomizationSettingsType['layout'],
|
|
||||||
event: ChangeEvent<HTMLInputElement>,
|
|
||||||
setState: Dispatch<SetStateAction<boolean>>
|
|
||||||
) => {
|
|
||||||
const value = event.target.checked;
|
|
||||||
setState(value);
|
|
||||||
updateConfig(configName, (prev) => {
|
|
||||||
const layout = prev.settings.customization.layout;
|
|
||||||
|
|
||||||
layout[key] = value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
layout,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className={classes.box} p="xl" pb="sm">
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Group spacing={5}>
|
|
||||||
<IconLayout size={20} />
|
|
||||||
<Title order={6}>Dashboard layout</Title>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Text color="dimmed" size="sm">
|
|
||||||
You can adjust the layout of the Dashboard to your preferences. The main are cannot be
|
|
||||||
turned on or off
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Paper px="xs" py={2} withBorder>
|
|
||||||
<Group position="apart">
|
|
||||||
<Logo size="xs" />
|
|
||||||
<Group spacing={4}>
|
|
||||||
{searchBar ? (
|
|
||||||
<Paper withBorder p={2} w={60}>
|
|
||||||
<Group spacing={2} align="center">
|
|
||||||
<IconSearch size={8} />
|
|
||||||
<Text size={8} color="dimmed">
|
|
||||||
Search
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
) : null}
|
|
||||||
{docker ? <IconBrandDocker size={18} color="#0db7ed" /> : null}
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Group align="stretch">
|
|
||||||
{leftSidebar && (
|
|
||||||
<Paper p="xs" withBorder>
|
|
||||||
<Center style={{ height: '100%' }}>
|
|
||||||
<Text align="center">Sidebar</Text>
|
|
||||||
</Center>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper className={classes.main} p="xs" withBorder>
|
|
||||||
<Text align="center">Main</Text>
|
|
||||||
<Text color="dimmed" size="xs" align="center">
|
|
||||||
Can be used for categories,
|
|
||||||
<br />
|
|
||||||
services and integrations
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{rightSidebar && (
|
|
||||||
<Paper p="xs" withBorder>
|
|
||||||
<Center style={{ height: '100%' }}>
|
|
||||||
<Text align="center">Sidebar</Text>
|
|
||||||
</Center>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Checkbox
|
|
||||||
label="Enable left sidebar"
|
|
||||||
description="Optional. Can be used for services and integrations only"
|
|
||||||
checked={leftSidebar}
|
|
||||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Enable right sidebar"
|
|
||||||
description="Optional. Can be used for services and integrations only"
|
|
||||||
checked={rightSidebar}
|
|
||||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Enable search bar"
|
|
||||||
checked={searchBar}
|
|
||||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Enable docker"
|
|
||||||
checked={docker}
|
|
||||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Enable pings"
|
|
||||||
checked={ping}
|
|
||||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
main: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
box: {
|
|
||||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
|
||||||
borderRadius: theme.radius.md,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface BackgroundChangerProps {
|
interface BackgroundChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -17,7 +17,7 @@ export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const backgroundImageUrl = value.trim().length === 0 ? undefined : value;
|
const backgroundImageUrl = value.trim().length === 0 ? undefined : value;
|
||||||
setBackgroundImageUrl(backgroundImageUrl);
|
setBackgroundImageUrl(backgroundImageUrl);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface FaviconChangerProps {
|
interface FaviconChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -17,7 +17,7 @@ export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
||||||
setFaviconUrl(faviconUrl);
|
setFaviconUrl(faviconUrl);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface LogoImageChangerProps {
|
interface LogoImageChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -17,7 +17,7 @@ export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
||||||
setLogoImageSrc(logoImageSrc);
|
setLogoImageSrc(logoImageSrc);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface MetaTitleChangerProps {
|
interface MetaTitleChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -18,7 +18,7 @@ export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const metaTitle = value.trim().length === 0 ? undefined : value;
|
const metaTitle = value.trim().length === 0 ? undefined : value;
|
||||||
setMetaTitle(metaTitle);
|
setMetaTitle(metaTitle);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextInput } from '@mantine/core';
|
import { TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface PageTitleChangerProps {
|
interface PageTitleChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -18,7 +18,7 @@ export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const pageTitle = value.trim().length === 0 ? undefined : value;
|
const pageTitle = value.trim().length === 0 ? undefined : value;
|
||||||
setPageTitle(pageTitle);
|
setPageTitle(pageTitle);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
ColorSwatch,
|
ColorSwatch,
|
||||||
Grid,
|
Grid,
|
||||||
@@ -8,11 +7,12 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useColorTheme } from '../../../tools/color';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
|
|
||||||
interface ColorControlProps {
|
interface ColorControlProps {
|
||||||
defaultValue: MantineTheme['primaryColor'] | undefined;
|
defaultValue: MantineTheme['primaryColor'] | undefined;
|
||||||
@@ -40,7 +40,7 @@ export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
|||||||
if (type === 'primary') setPrimaryColor(color);
|
if (type === 'primary') setPrimaryColor(color);
|
||||||
else setSecondaryColor(color);
|
else setSecondaryColor(color);
|
||||||
updateConfig(configName, (prev) => {
|
updateConfig(configName, (prev) => {
|
||||||
const colors = prev.settings.customization.colors;
|
const { colors } = prev.settings.customization;
|
||||||
colors[type] = color;
|
colors[type] = color;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Textarea } from '@mantine/core';
|
import { Textarea } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface CustomCssChangerProps {
|
interface CustomCssChangerProps {
|
||||||
defaultValue: string | undefined;
|
defaultValue: string | undefined;
|
||||||
@@ -17,7 +17,7 @@ export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => {
|
|||||||
if (!configName) return null;
|
if (!configName) return null;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
||||||
const value = ev.currentTarget.value;
|
const { value } = ev.currentTarget;
|
||||||
const customCss = value.trim().length === 0 ? undefined : value;
|
const customCss = value.trim().length === 0 ? undefined : value;
|
||||||
setCustomCss(customCss);
|
setCustomCss(customCss);
|
||||||
updateConfig(configName, (prev) => ({
|
updateConfig(configName, (prev) => ({
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import { Slider, Stack, Text } from '@mantine/core';
|
||||||
import { Text, Slider, Stack } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useState } from 'react';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
|
||||||
interface OpacitySelectorProps {
|
interface OpacitySelectorProps {
|
||||||
defaultValue: number | undefined;
|
defaultValue: number | undefined;
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
ColorSwatch,
|
ColorSwatch,
|
||||||
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
MantineTheme,
|
||||||
Popover,
|
Popover,
|
||||||
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
MantineTheme,
|
|
||||||
Stack,
|
|
||||||
Grid,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useColorTheme } from '../../../tools/color';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useConfigStore } from '../../../config/store';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useState } from 'react';
|
||||||
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
|
|
||||||
interface ShadeSelectorProps {
|
interface ShadeSelectorProps {
|
||||||
defaultValue: MantineTheme['primaryShade'] | undefined;
|
defaultValue: MantineTheme['primaryShade'] | undefined;
|
||||||
@@ -4,9 +4,9 @@ import { useState } from 'react';
|
|||||||
import { IconSettings } from '@tabler/icons';
|
import { IconSettings } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import CustomizationSettings from './CustomizationSettings';
|
import CustomizationSettings from './Customization/CustomizationSettings';
|
||||||
import CommonSettings from './CommonSettings';
|
import CommonSettings from './Common/CommonSettings';
|
||||||
import Credits from './Credits';
|
import Credits from './Common/Credits';
|
||||||
|
|
||||||
function SettingsMenu() {
|
function SettingsMenu() {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useConfigContext } from '../../config/provider';
|
|||||||
import { Background } from './Background';
|
import { Background } from './Background';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header/Header';
|
import { Header } from './Header/Header';
|
||||||
import { Head } from './Head/Head';
|
import { Head } from './Header/Meta/Head';
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({}));
|
const useStyles = createStyles(() => ({}));
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) {
|
|||||||
return (
|
return (
|
||||||
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
||||||
<Image
|
<Image
|
||||||
width={size === 'md' ? 50 : 20}
|
width={size === 'md' ? 50 : 12}
|
||||||
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo.png'}
|
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo.png'}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -22,7 +22,7 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) {
|
|||||||
/>
|
/>
|
||||||
{withoutText ? null : (
|
{withoutText ? null : (
|
||||||
<Text
|
<Text
|
||||||
size={size === 'md' ? 22 : 'xs'}
|
size={size === 'md' ? 22 : 10}
|
||||||
weight="bold"
|
weight="bold"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={primaryGradient}
|
gradient={primaryGradient}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { ActionIcon, Tooltip } from '@mantine/core';
|
|||||||
import { openContextModal } from '@mantine/modals';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { IconApps } from '@tabler/icons';
|
import { IconApps } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions';
|
|
||||||
import { ServiceType } from '../../../../../types/service';
|
|
||||||
|
|
||||||
export const AddElementAction = () => {
|
export const AddElementAction = () => {
|
||||||
const { t } = useTranslation('layout/add-service-app-shelf');
|
const { t } = useTranslation('layout/add-service-app-shelf');
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Button, Popover, Tooltip, Text } from '@mantine/core';
|
||||||
|
import { IconEditCircle, IconEditCircleOff } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore';
|
||||||
|
|
||||||
|
export const ToggleEditModeAction = () => {
|
||||||
|
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
||||||
|
|
||||||
|
const { enabled, toggleEditMode } = useEditModeStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('tooltip')} withinPortal>
|
||||||
|
<Popover opened={enabled} width={250} withArrow>
|
||||||
|
<Popover.Target>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleEditMode()}
|
||||||
|
leftIcon={enabled ? <IconEditCircleOff /> : <IconEditCircle />}
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
color="blue"
|
||||||
|
style={{ height: 'auto', alignSelf: 'stretch' }}
|
||||||
|
>
|
||||||
|
{enabled ? t('button.enabled') : t('button.disabled')}
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Text align="center">
|
||||||
|
<Text weight="bold">{t('popover.title')}</Text>
|
||||||
|
{t('popover.text')}
|
||||||
|
</Text>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader, useMantineTheme } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader, Text } from '@mantine/core';
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
|
|
||||||
import DockerMenuButton from '../../../modules/docker/DockerModule';
|
|
||||||
import { Search } from './Search';
|
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import { SettingsMenu } from './SettingsMenu';
|
|
||||||
import { ToolsMenu } from './ToolsMenu';
|
|
||||||
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
|
import { AddElementAction } from './Actions/AddElementAction/AddElementAction';
|
||||||
import { ViewToggleButton } from '../../Dashboard/Views/ViewToggleButton';
|
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
|
||||||
|
import { Search } from './Search';
|
||||||
|
import { SettingsMenu } from './SettingsMenu';
|
||||||
|
|
||||||
export const HeaderHeight = 64;
|
export const HeaderHeight = 64;
|
||||||
|
|
||||||
@@ -17,6 +14,8 @@ export function Header(props: any) {
|
|||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const { classes: cardClasses } = useCardStyles();
|
const { classes: cardClasses } = useCardStyles();
|
||||||
|
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
|
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
|
||||||
<Group p="xs" noWrap grow>
|
<Group p="xs" noWrap grow>
|
||||||
@@ -24,11 +23,24 @@ export function Header(props: any) {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
<Group position="right" noWrap>
|
<Group position="right" noWrap>
|
||||||
|
<Text
|
||||||
|
onClick={() => {
|
||||||
|
openContextModal({
|
||||||
|
modal: 'changeTilePosition',
|
||||||
|
title: 'Change tile position',
|
||||||
|
innerProps: {
|
||||||
|
tile: config?.services[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="link"
|
||||||
|
>
|
||||||
|
Test: Open Change Pos Modal
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Search />
|
<Search />
|
||||||
<AddItemShelfButton />
|
|
||||||
<AddElementAction />
|
<AddElementAction />
|
||||||
<ToolsMenu />
|
<ToggleEditModeAction />
|
||||||
<ViewToggleButton />
|
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NextHead from 'next/head';
|
import NextHead from 'next/head';
|
||||||
import { SafariStatusBarStyle } from './SafariStatusBarStyle';
|
import { SafariStatusBarStyle } from './SafariStatusBarStyle';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
|
|
||||||
export function Head() {
|
export function Head() {
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
@@ -18,7 +18,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||||
import SmallServiceItem from '../../AppShelf/SmallServiceItem';
|
import SmallServiceItem from '../../AppShelf/SmallServiceItem';
|
||||||
import Tip from '../Tip';
|
import Tip from '../Tip';
|
||||||
import { searchUrls } from '../../Settings/Common/SearchEngineSelector';
|
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
|
||||||
import { useConfigContext } from '../../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { OverseerrMediaDisplay } from '../../../modules/common';
|
import { OverseerrMediaDisplay } from '../../../modules/common';
|
||||||
import { IModule } from '../../../modules/ModuleTypes';
|
import { IModule } from '../../../modules/ModuleTypes';
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ActionIcon, Menu, Text } from '@mantine/core';
|
|
||||||
import { IconAxe } from '@tabler/icons';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import DockerMenuButton from '../../../modules/docker/DockerModule';
|
|
||||||
|
|
||||||
export const ToolsMenu = () => {
|
|
||||||
const { t } = useTranslation('layout/tools');
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="default" radius="md" size="xl" color="blue">
|
|
||||||
<IconAxe />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
{/* TODO: Implement check to display fallback when no tools */}
|
|
||||||
<DockerMenuButton />
|
|
||||||
<Menu.Item closeMenuOnClick={false} disabled>
|
|
||||||
<Text size="sm" color="dimmed">
|
|
||||||
{t('fallback.title')}
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -16,9 +16,11 @@ import Dockerode from 'dockerode';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TFunction } from 'react-i18next';
|
import { TFunction } from 'react-i18next';
|
||||||
import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { tryMatchService } from '../../tools/addToHomarr';
|
import { tryMatchService } from '../../tools/addToHomarr';
|
||||||
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
|
import { ServiceType } from '../../types/service';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
|
|
||||||
@@ -160,21 +162,40 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
radius="md"
|
radius="md"
|
||||||
disabled={selected.length === 0 || selected.length > 1}
|
disabled={selected.length === 0 || selected.length > 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openModal({
|
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
|
||||||
size: 'xl',
|
openContextModalGeneric<{ service: ServiceType }>({
|
||||||
modalId: selected.at(0)!.Id,
|
modal: 'editService',
|
||||||
radius: 'md',
|
innerProps: {
|
||||||
title: t('actionBar.addService.title'),
|
service: {
|
||||||
zIndex: 500,
|
id: uuidv4(),
|
||||||
children: (
|
name: selected[0].Names[0],
|
||||||
<AddAppShelfItemForm
|
url: containerUrl,
|
||||||
setConfig={setConfig}
|
appearance: {
|
||||||
config={config}
|
iconUrl: '/imgs/logo/logo.png', // TODO: find icon automatically
|
||||||
setOpened={() => closeModal(selected.at(0)!.Id)}
|
},
|
||||||
message={t('actionBar.addService.message')}
|
network: {
|
||||||
{...tryMatchService(selected.at(0)!)}
|
enabledStatusChecker: false,
|
||||||
/>
|
okStatus: [],
|
||||||
),
|
},
|
||||||
|
behaviour: {
|
||||||
|
isOpeningNewTab: true,
|
||||||
|
onClickUrl: '',
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: 'wrapper', // TODO: Set the wrapper automatically
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
location: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ export default function TorrentsComponent() {
|
|||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||||
<AddItemShelfButton />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { Datum, ResponsiveLine } from '@nivo/line';
|
import { Datum, ResponsiveLine } from '@nivo/line';
|
||||||
import { useListState } from '@mantine/hooks';
|
import { useListState } from '@mantine/hooks';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
@@ -84,11 +83,6 @@ export default function TotalDownloadsComponent() {
|
|||||||
<Group>
|
<Group>
|
||||||
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={4}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<div>
|
<div>
|
||||||
<AddItemShelfButton
|
|
||||||
style={{
|
|
||||||
float: 'inline-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{t('card.errors.noDownloadClients.text')}
|
{t('card.errors.noDownloadClients.text')}
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { UsenetHistoryList } from './UsenetHistoryList';
|
|||||||
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
||||||
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||||
<AddItemShelfButton />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Head from 'next/head';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IntegrationsEditModal } from '../components/Dashboard/Tiles/IntegrationsEditModal';
|
import { IntegrationsEditModal } from '../components/Dashboard/Tiles/IntegrationsEditModal';
|
||||||
import { IntegrationRemoveModal } from '../components/Dashboard/Tiles/IntegrationRemoveModal';
|
import { IntegrationRemoveModal } from '../components/Dashboard/Tiles/IntegrationRemoveModal';
|
||||||
|
import { ChangePositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangePositionModal';
|
||||||
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
|
||||||
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
@@ -91,6 +92,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
integrationRemove: IntegrationRemoveModal,
|
integrationRemove: IntegrationRemoveModal,
|
||||||
integrationChangePosition: IntegrationChangePositionModal,
|
integrationChangePosition: IntegrationChangePositionModal,
|
||||||
categoryEditModal: CategoryEditModal,
|
categoryEditModal: CategoryEditModal,
|
||||||
|
changeTilePosition: ChangePositionModal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
|||||||
26
src/tools/hooks/useRepositoryIconsQuery.ts
Normal file
26
src/tools/hooks/useRepositoryIconsQuery.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { IconSelectorItem } from '../../types/iconSelector/iconSelectorItem';
|
||||||
|
|
||||||
|
export const useRepositoryIconsQuery = <TRepositoryIcon extends object>({
|
||||||
|
url,
|
||||||
|
converter,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
converter: (value: TRepositoryIcon) => IconSelectorItem;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['repository-icons', { url }],
|
||||||
|
queryFn: async () => fetchRepositoryIcons<TRepositoryIcon>(url),
|
||||||
|
select(data) {
|
||||||
|
return data.map(x => converter(x));
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRepositoryIcons =
|
||||||
|
async <TRepositoryIcon extends object>(url: string): Promise<TRepositoryIcon[]> => {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png'
|
||||||
|
);
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ export const dashboardNamespaces = [
|
|||||||
'layout/app-shelf-menu',
|
'layout/app-shelf-menu',
|
||||||
'layout/tools',
|
'layout/tools',
|
||||||
'layout/element-selector/selector',
|
'layout/element-selector/selector',
|
||||||
|
'layout/header/actions/toggle-edit-mode',
|
||||||
'settings/common',
|
'settings/common',
|
||||||
'settings/general/theme-selector',
|
'settings/general/theme-selector',
|
||||||
'settings/general/config-changer',
|
'settings/general/config-changer',
|
||||||
|
|||||||
3
src/types/iconSelector/iconSelectorItem.ts
Normal file
3
src/types/iconSelector/iconSelectorItem.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IconSelectorItem {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface WalkxcodeRepositoryIcon {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export interface ServiceType extends TileBaseType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceBehaviourType {
|
interface ServiceBehaviourType {
|
||||||
onClickUrl?: string;
|
onClickUrl: string;
|
||||||
isOpeningNewTab: boolean;
|
isOpeningNewTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user