Add disable edit mode environment variable (#730)

This commit is contained in:
Manuel
2023-02-22 21:59:49 +01:00
committed by GitHub
parent f5686fbf2c
commit f56f4b33ce
7 changed files with 103 additions and 18 deletions

View File

@@ -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"
} }
} }

View File

@@ -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;

View File

@@ -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}

View File

@@ -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={

View 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 })),
}));

View File

@@ -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);

View File

@@ -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) {