diff --git a/database/022-create-maintenance-tables.sql b/database/022-create-maintenance-tables.sql new file mode 100644 index 000000000..a6812a1be --- /dev/null +++ b/database/022-create-maintenance-tables.sql @@ -0,0 +1,43 @@ +-- SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +-- SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +-- +-- SPDX-License-Identifier: Apache-2.0 +-- SPDX-License-Identifier: EUPL-1.2 + +CREATE TABLE global_announcement ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + text VARCHAR(200), + enabled BOOLEAN DEFAULT FALSE NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE FUNCTION sanitise_insert_global_announcement () RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.id = gen_random_uuid(); + NEW.created_at = LOCALTIMESTAMP; + NEW.updated_at = NEW.created_at; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_insert_global_announcement BEFORE INSERT ON global_announcement FOR EACH ROW EXECUTE PROCEDURE sanitise_insert_global_announcement(); + +CREATE FUNCTION sanitise_update_global_announcement() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.id = OLD.id; + NEW.created_at = OLD.created_at; + NEW.updated_at = LOCALTIMESTAMP; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_update_global_announcement BEFORE UPDATE ON global_announcement FOR EACH ROW EXECUTE PROCEDURE sanitise_update_global_announcement(); + +ALTER TABLE global_announcement ENABLE ROW LEVEL SECURITY; + +CREATE POLICY anyone_can_read ON global_announcement FOR SELECT TO rsd_web_anon, rsd_user USING (TRUE);´ + +CREATE POLICY admin_all_rights ON global_announcement TO rsd_admin USING (TRUE) WITH CHECK (TRUE); diff --git a/frontend/components/Announcement/Announcement.tsx b/frontend/components/Announcement/Announcement.tsx new file mode 100644 index 000000000..7a9b7d2c7 --- /dev/null +++ b/frontend/components/Announcement/Announcement.tsx @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import Button from '@mui/material/Button' +import CancelIcon from '@mui/icons-material/Cancel' +import ErrorIcon from '@mui/icons-material/Error' +import {useState} from 'react' + +export default function Announcement({announcement}: {announcement: string | null}) { + const [open, setOpen] = useState(true) + if (announcement === null || announcement === undefined) return null + + if (!open) return null + + return ( +
+
+ +
+
+ {announcement} +
+ +
+ ) +} diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index db9d8d07d..78b827c9f 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -1,5 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // @@ -21,6 +23,12 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle' import FluorescentIcon from '@mui/icons-material/Fluorescent' export const adminPages = { + notifications: { + title: 'Announcements', + subtitle: 'Show global notifications to all users', + icon: , + path: '/admin/announcements', + }, pages:{ title: 'Public pages', subtitle: 'Manage markdown pages', diff --git a/frontend/components/admin/announcements/AnnouncementsForm.tsx b/frontend/components/admin/announcements/AnnouncementsForm.tsx new file mode 100644 index 000000000..625f5880d --- /dev/null +++ b/frontend/components/admin/announcements/AnnouncementsForm.tsx @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import Button from '@mui/material/Button' +import AddIcon from '@mui/icons-material/Add' + +import {useSession} from '~/auth' +import {Controller, SubmitHandler, UseControllerProps, useController, useForm} from 'react-hook-form' +import AutosaveControlledTextField, {OnSaveProps} from '~/components/form/AutosaveControlledTextField' +import {Input, Switch} from '@mui/material' +import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import {GetServerSidePropsContext} from 'next' +import logger from '~/utils/logger' +import {useEffect, useState} from 'react' +import ControlledSwitch from '~/components/form/ControlledSwitch' +import ControlledTextField from '~/components/form/ControlledTextField' + +const formId = 'announcements-form' + +type EditAnnouncementItem = { + [id: string]: any, + text: string | null, + enabled: boolean +} + +type EditAnnouncementFormProps = { + data: EditAnnouncementItem +} + +export default function AnnouncementsForm({data}: EditAnnouncementFormProps) { + const {token} = useSession() + const {handleSubmit, register, control, setValue, formState: {errors}} = useForm({ + defaultValues: { + ...data + }, + mode: 'onChange' + }) + + function onSubmit(dataToSubmit: EditAnnouncementItem) { + saveAnnouncement(dataToSubmit).then( + (resp) => { + if (resp && [200, 201].includes(resp.status) && resp?.object) { + const newObj = resp.object[0] + setValue('enabled', newObj.enabled) + setValue('id', newObj.id) + setValue('text', newObj.text) + } else { + console.log('Error saving data:', resp) + } + } + ) + } + + async function saveAnnouncement(item: EditAnnouncementItem) { + try { + let method + let url + if (item.id == '') { + delete item.id + method = 'POST' + url = '/api/v1/global_announcement' + } else { + method = 'PATCH' + url = `/api/v1/global_announcement?id=eq.${item.id}` + } + const resp = await fetch(url, { + method: method, + headers: { + ...createJsonHeaders(token), + Prefer: 'return=representation', + }, + body: JSON.stringify(item) + }) + if ([200, 201].includes(resp.status)) { + return { + status: 201, + object: await resp.json() + } + } + // return extractReturnMessage(resp, item.text ?? '') + } catch (e: any) { + logger(`saveAnnouncement: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } + } + + return ( +
+ + + + + + ) +} diff --git a/frontend/components/admin/announcements/getAnnouncement.tsx b/frontend/components/admin/announcements/getAnnouncement.tsx new file mode 100644 index 000000000..2827c548b --- /dev/null +++ b/frontend/components/admin/announcements/getAnnouncement.tsx @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import {getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' + +export default async function getAnnouncement() { + const url = getBaseUrl() + '/global_announcement' + const resp = await fetch(url, {method: 'GET',}) + const json = await resp.json() + if (json[0] && json[0].enabled) { + return json[0].text + } else { + logger('getAnnouncement failed:') + return null + } +} diff --git a/frontend/components/admin/announcements/index.tsx b/frontend/components/admin/announcements/index.tsx new file mode 100644 index 000000000..e550bd704 --- /dev/null +++ b/frontend/components/admin/announcements/index.tsx @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import Button from '@mui/material/Button' +import AddIcon from '@mui/icons-material/Add' + +import {useSession} from '~/auth' +import {SubmitHandler, UseControllerProps, useController, useForm} from 'react-hook-form' +import AutosaveControlledTextField, {OnSaveProps} from '~/components/form/AutosaveControlledTextField' +import {Input, Switch} from '@mui/material' +import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import {GetServerSidePropsContext} from 'next' +import logger from '~/utils/logger' +import {useEffect, useState} from 'react' +import AnnouncementsForm from './AnnouncementsForm' +import ContentLoader from '~/components/layout/ContentLoader' + +const formId = 'announcements-form' + +type EditAnnouncementItem = { + [id: string]: any, + text: string | null, + enabled: boolean +} + + +function getData({token}: {token: string}) { + const [loading, setLoading] = useState(true) + const [item, setItem] = useState() + + async function getAnnouncement() { + const url = getBaseUrl() + '/global_announcement' + setLoading(true) + const resp = await fetch(url, { + method: 'GET', + headers: { + ...createJsonHeaders(token), + } + }) + + if (resp.status === 200) { + const json = await resp.json() + if (json[0]) { + setItem({ + id: json[0].id, + enabled: json[0].enabled, + text: json[0].text + }) + } else { + setItem({ + enabled: false, + text: '' + }) + } + setLoading(false) + } + } + + useEffect(() => { + getAnnouncement() + }, []) + + return { + formProps: item, + loading + } + +} + + +export default function AnnouncementsPage() { + const {token} = useSession() + const {formProps, loading} = getData({token}) + + if (loading) return ( + + ) + + if (formProps) { + return ( + + ) + } + + return +} diff --git a/frontend/config/getSettingsServerSide.ts b/frontend/config/getSettingsServerSide.ts index 987fc1424..5566bd43a 100644 --- a/frontend/config/getSettingsServerSide.ts +++ b/frontend/config/getSettingsServerSide.ts @@ -1,7 +1,10 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 import {IncomingMessage} from 'http' import {ParsedUrlQuery} from 'querystring' @@ -10,6 +13,7 @@ import logger from '~/utils/logger' import {getPageLinks} from '~/components/admin/pages/useMarkdownPages' import {defaultRsdSettings, RsdSettingsState} from './rsdSettingsReducer' import defaultSettings from '~/config/defaultSettings.json' +import getAnnouncement from '~/components/admin/announcements/getAnnouncement' /** * getThemeSettings from local json file @@ -42,6 +46,8 @@ export async function getSettingsServerSide(req: IncomingMessage | undefined, qu if (typeof req === 'undefined') return defaultRsdSettings as RsdSettingsState // get links const pages = await getPageLinks({is_published: true}) + // get announcments + const announcement = await getAnnouncement() // extract embed flag const embed = typeof query?.embed !== 'undefined' // get settings (host and theme) @@ -54,6 +60,7 @@ export async function getSettingsServerSide(req: IncomingMessage | undefined, qu theme: settings.theme, pages, embed, + announcement: announcement } // console.group('getSettingsServerSide') // console.log('rsdSettings...', rsdSettings) diff --git a/frontend/config/rsdSettingsReducer.ts b/frontend/config/rsdSettingsReducer.ts index e28b7bac7..134dcfc58 100644 --- a/frontend/config/rsdSettingsReducer.ts +++ b/frontend/config/rsdSettingsReducer.ts @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 import logger from '~/utils/logger' import {RsdTheme} from '~/styles/rsdMuiTheme' @@ -14,7 +15,8 @@ export type RsdSettingsState = { embed: boolean links?: CustomLink[] pages?: RsdLink[] - theme: RsdTheme + theme: RsdTheme, + announcement: string | null } export type RsdHost = { @@ -69,6 +71,7 @@ export const defaultRsdSettings: RsdSettingsState = { embed: false, theme: defaultSettings.theme, links:[], + announcement: null } export function rsdSettingsReducer(state: RsdSettingsState, action: RsdSettingsAction) { diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 462ab4d67..b5a36ee61 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -2,8 +2,11 @@ // SPDX-FileCopyrightText: 2021 - 2023 dv4all // SPDX-FileCopyrightText: 2022 Jesús García Gonzalez (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 import {useEffect, useState, useMemo} from 'react' import Router, {useRouter} from 'next/router' @@ -32,6 +35,7 @@ import {getMatomoConsent,Matomo} from '~/components/cookies/nodeCookies' import {initMatomoCustomUrl} from '~/components/cookies/setMatomoPage' import {getSettingsServerSide} from '~/config/getSettingsServerSide' import {setContentSecurityPolicyHeader} from '~/utils/contentSecurityPolicy' +import Announcement from '~/components/Announcement/Announcement' // extend Next app props interface with emotion cache export interface MuiAppProps extends AppProps { @@ -69,6 +73,7 @@ function RsdApp(props: MuiAppProps) { Component, emotionCache = clientSideEmotionCache, pageProps, session, settings, matomo } = props + const announcement = settings.announcement //currently we support only default (light) and dark RSD theme for MUI // const muiTheme = loadMuiTheme(settings.theme.mode as RsdThemes) const router = useRouter() @@ -128,6 +133,7 @@ function RsdApp(props: MuiAppProps) { {/* Matomo cookie consent dialog */} + ) diff --git a/frontend/pages/admin/announcements.tsx b/frontend/pages/admin/announcements.tsx new file mode 100644 index 000000000..02f05478f --- /dev/null +++ b/frontend/pages/admin/announcements.tsx @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import Head from 'next/head' + +import {app} from '../../config/app' +import DefaultLayout from '~/components/layout/DefaultLayout' +import AdminPageWithNav from '~/components/admin/AdminPageWithNav' +import {adminPages} from '~/components/admin/AdminNav' +import AnnouncementsPage from '~/components/admin/announcements/index' + + +const pageTitle = `${adminPages['keywords'].title} | Admin page | ${app.title}` + +export default function AdminAnnouncementsPage(props:any) { + + return ( + + + {pageTitle} + + + + + + ) +}