mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 06:55:51 +01:00
✨ Add disable edit mode environment variable (#730)
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"version": "Version",
|
"version": "Version",
|
||||||
"nodeEnvironment": "Node environment",
|
"nodeEnvironment": "Node environment",
|
||||||
"i18n": "Loaded I18n translation namespaces",
|
"i18n": "Loaded I18n translation namespaces",
|
||||||
"locales": "Configured I18n locales"
|
"locales": "Configured I18n locales",
|
||||||
|
"experimental_disableEditMode": "<b>EXPERIMENTAL</b>: Disable edit mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
Modal,
|
Modal,
|
||||||
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
IconBrandDiscord,
|
IconBrandDiscord,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconFile,
|
IconFile,
|
||||||
|
IconKey,
|
||||||
IconLanguage,
|
IconLanguage,
|
||||||
IconSchema,
|
IconSchema,
|
||||||
IconVersions,
|
IconVersions,
|
||||||
@@ -31,6 +33,7 @@ import Image from 'next/image';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
|
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
import { usePrimaryGradient } from '../../../layout/useGradient';
|
||||||
import Credits from '../../../Settings/Common/Credits';
|
import Credits from '../../../Settings/Common/Credits';
|
||||||
@@ -82,10 +85,17 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
<ActionIcon className={classes.informationIcon} variant="default">
|
<ActionIcon className={classes.informationIcon} variant="default">
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
{t(`layout/modals/about:metrics.${item.label}`)}
|
<Text>
|
||||||
|
<Trans
|
||||||
|
i18nKey={`layout/modals/about:metrics.${item.label}`}
|
||||||
|
components={{ b: <b /> }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</td>
|
||||||
<td className={classes.informationTableColumn}>{item.content}</td>
|
<td className={classes.informationTableColumn} style={{ maxWidth: 200 }}>
|
||||||
|
{item.content}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -153,12 +163,34 @@ interface ExtendedInitOptions extends InitOptions {
|
|||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||||
const colorGradiant = usePrimaryGradient();
|
const colorGradiant = usePrimaryGradient();
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
const { configVersion } = useConfigContext();
|
||||||
const { configs } = useConfigStore();
|
const { configs } = useConfigStore();
|
||||||
|
|
||||||
let items: InformationTableItem[] = [];
|
let items: InformationTableItem[] = [];
|
||||||
|
|
||||||
|
if (editModeEnabled) {
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
{
|
||||||
|
icon: <IconKey size={20} />,
|
||||||
|
label: 'experimental_disableEditMode',
|
||||||
|
content: (
|
||||||
|
<Stack>
|
||||||
|
<Badge color="red">WARNING</Badge>
|
||||||
|
<Text color="red" size="xs">
|
||||||
|
This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||||
|
modifications are possbile anymore. All update requests for the config will be dropped
|
||||||
|
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||||
|
authentication system, which will make this obsolete.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (i18n !== null) {
|
if (i18n !== null) {
|
||||||
const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces();
|
const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces();
|
||||||
const initOptions = i18n.options as ExtendedInitOptions;
|
const initOptions = i18n.options as ExtendedInitOptions;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
import { Box, createStyles, Group, Header as MantineHeader, Indicator } from '@mantine/core';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { REPO_URL } from '../../../../data/constants';
|
import { REPO_URL } from '../../../../data/constants';
|
||||||
|
import { useEditModeInformationStore } from '../../../hooks/useEditModeInformation';
|
||||||
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
import DockerMenuButton from '../../../modules/Docker/DockerModule';
|
||||||
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Logo';
|
||||||
@@ -15,8 +16,9 @@ export function Header(props: any) {
|
|||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const { classes: cardClasses, cx } = useCardStyles(false);
|
const { classes: cardClasses, cx } = useCardStyles(false);
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
|
||||||
const { isLoading, error, data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['github/latest'],
|
queryKey: ['github/latest'],
|
||||||
cacheTime: 1000 * 60 * 60 * 24,
|
cacheTime: 1000 * 60 * 60 * 24,
|
||||||
staleTime: 1000 * 60 * 60 * 5,
|
staleTime: 1000 * 60 * 60 * 5,
|
||||||
@@ -32,9 +34,14 @@ export function Header(props: any) {
|
|||||||
<Box className={cx(classes.hide, 'dashboard-header-logo-root')}>
|
<Box className={cx(classes.hide, 'dashboard-header-logo-root')}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
<Group className="dashboard-header-group-right" position="right" style={{ maxWidth: 'none' }} noWrap>
|
<Group
|
||||||
|
className="dashboard-header-group-right"
|
||||||
|
position="right"
|
||||||
|
style={{ maxWidth: 'none' }}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
<Search />
|
<Search />
|
||||||
<ToggleEditModeAction />
|
{!editModeEnabled && <ToggleEditModeAction />}
|
||||||
<DockerMenuButton />
|
<DockerMenuButton />
|
||||||
<Indicator
|
<Indicator
|
||||||
size={15}
|
size={15}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Badge, Button, Menu } from '@mantine/core';
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
|
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useEditModeInformationStore } from '../../../hooks/useEditModeInformation';
|
||||||
import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
|
import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
|
||||||
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
@@ -12,6 +13,7 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
|||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const [aboutModalOpened, aboutModal] = useDisclosure(false);
|
const [aboutModalOpened, aboutModal] = useDisclosure(false);
|
||||||
const { classes } = useCardStyles(true);
|
const { classes } = useCardStyles(true);
|
||||||
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,9 +26,11 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
|||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
{!editModeEnabled && (
|
||||||
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
||||||
{t('sections.settings')}
|
{t('sections.settings')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
)}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<IconInfoCircle strokeWidth={1.2} size={18} />}
|
icon={<IconInfoCircle strokeWidth={1.2} size={18} />}
|
||||||
rightSection={
|
rightSection={
|
||||||
|
|||||||
11
src/hooks/useEditModeInformation.ts
Normal file
11
src/hooks/useEditModeInformation.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface EditModeInformationStore {
|
||||||
|
editModeEnabled: boolean;
|
||||||
|
setDisabled: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEditModeInformationStore = create<EditModeInformationStore>((set) => ({
|
||||||
|
editModeEnabled: false,
|
||||||
|
setDisabled: () => set(() => ({ editModeEnabled: true })),
|
||||||
|
}));
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from '@mantine/core';
|
import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from '@mantine/core';
|
||||||
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
||||||
import { ModalsProvider } from '@mantine/modals';
|
import { ModalsProvider } from '@mantine/modals';
|
||||||
|
import Consola from 'consola';
|
||||||
import { NotificationsProvider } from '@mantine/notifications';
|
import { NotificationsProvider } from '@mantine/notifications';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { getCookie } from 'cookies-next';
|
import { getCookie } from 'cookies-next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import 'video.js/dist/video-js.css';
|
||||||
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
||||||
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
||||||
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
|
import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
|
||||||
@@ -18,21 +20,25 @@ import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsE
|
|||||||
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||||
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
|
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
import { queryClient } from '../tools/queryClient';
|
import { queryClient } from '../tools/queryClient';
|
||||||
import { theme } from '../tools/theme';
|
|
||||||
import {
|
import {
|
||||||
getServiceSidePackageAttributes,
|
getServiceSidePackageAttributes,
|
||||||
ServerSidePackageAttributesType,
|
ServerSidePackageAttributesType,
|
||||||
} from '../tools/server/getPackageVersion';
|
} from '../tools/server/getPackageVersion';
|
||||||
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
import { theme } from '../tools/theme';
|
||||||
import 'video.js/dist/video-js.css';
|
|
||||||
|
|
||||||
|
import { useEditModeInformationStore } from '../hooks/useEditModeInformation';
|
||||||
import '../styles/global.scss';
|
import '../styles/global.scss';
|
||||||
|
|
||||||
function App(
|
function App(
|
||||||
this: any,
|
this: any,
|
||||||
props: AppProps & { colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType }
|
props: AppProps & {
|
||||||
|
colorScheme: ColorScheme;
|
||||||
|
packageAttributes: ServerSidePackageAttributesType;
|
||||||
|
editModeEnabled: boolean;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
const { Component, pageProps } = props;
|
const { Component, pageProps } = props;
|
||||||
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>('red');
|
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>('red');
|
||||||
@@ -57,9 +63,14 @@ function App(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { setInitialPackageAttributes } = usePackageAttributesStore();
|
const { setInitialPackageAttributes } = usePackageAttributesStore();
|
||||||
|
const { setDisabled } = useEditModeInformationStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInitialPackageAttributes(props.packageAttributes);
|
setInitialPackageAttributes(props.packageAttributes);
|
||||||
|
|
||||||
|
if (!props.editModeEnabled) {
|
||||||
|
setDisabled();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleColorScheme = (value?: ColorScheme) =>
|
const toggleColorScheme = (value?: ColorScheme) =>
|
||||||
@@ -125,9 +136,19 @@ function App(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
|
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||||
|
const disableEditMode =
|
||||||
|
process.env.DISABLE_EDIT_MODE && process.env.DISABLE_EDIT_MODE.toLowerCase() === 'true';
|
||||||
|
if (disableEditMode) {
|
||||||
|
Consola.warn(
|
||||||
|
'EXPERIMENTAL: You have disabled the edit mode. Modifications are no longer possible and any requests on the API will be dropped. If you want to disable this, unset the DISABLE_EDIT_MODE environment variable. This behaviour may be removed in future versions of Homarr'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
||||||
packageAttributes: getServiceSidePackageAttributes(),
|
packageAttributes: getServiceSidePackageAttributes(),
|
||||||
});
|
editModeEnabled: !disableEditMode,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default appWithTranslation(App);
|
export default appWithTranslation(App);
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import { BackendConfigType, ConfigType } from '../../../types/config';
|
import { BackendConfigType, ConfigType } from '../../../types/config';
|
||||||
import { getConfig } from '../../../tools/config/getConfig';
|
import { getConfig } from '../../../tools/config/getConfig';
|
||||||
|
|
||||||
function Put(req: NextApiRequest, res: NextApiResponse) {
|
function Put(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (process.env.DISABLE_EDIT_MODE === 'true') {
|
||||||
|
return res.status(409).json({ error: 'Edit mode has been disabled by the administrator' });
|
||||||
|
}
|
||||||
|
|
||||||
// Get the slug of the request
|
// Get the slug of the request
|
||||||
const { slug } = req.query as { slug: string };
|
const { slug } = req.query as { slug: string };
|
||||||
|
|
||||||
// Get the body of the request
|
// Get the body of the request
|
||||||
const { body: config }: { body: ConfigType } = req;
|
const { body: config }: { body: ConfigType } = req;
|
||||||
if (!slug || !config) {
|
if (!slug || !config) {
|
||||||
|
|||||||
Reference in New Issue
Block a user