From 02ceffccb522affed2d166bef96c7741500d26c3 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Wed, 24 Jul 2024 09:47:01 +0200 Subject: [PATCH] feat: add incidents and maintenance section for admins (#3220) Closes #3219. Details: * Add a new "Incidents and Maintenance" section in the admin page * Revamp how the status banner and status summary work in Renku --- client/package.json | 3 +- .../src/components/navbar/AnonymousNavBar.tsx | 7 +- .../src/components/navbar/LoggedInNavBar.tsx | 17 +- client/src/features/admin/AdminPage.tsx | 2 + .../admin/IncidentsAndMaintenanceSection.tsx | 326 ++++++++++++++++ client/src/features/dashboardV2/HelpV2.tsx | 10 +- .../platform/api/platform-empty.api.ts | 26 ++ .../platform/api/platform.api-config.ts | 32 ++ .../src/features/platform/api/platform.api.ts | 34 ++ .../platform/api/platform.generated-api.ts | 52 +++ .../platform/api/platform.openapi.json | 161 ++++++++ .../platform/components/StatusBanner.tsx | 350 ++++++++++++++++++ .../components/StatusPageIncidentUpdates.tsx | 63 ++++ .../platform/components/StatusSummary.tsx | 339 +++++++++++++++++ .../statuspage-api/statuspage-empty.api.ts | 26 ++ .../platform/statuspage-api/statuspage.api.ts | 35 ++ .../statuspage-api/statuspage.types.ts | 116 ++++++ client/src/features/rootV2/NavbarV2.tsx | 160 ++++---- client/src/help/Help.tsx | 6 +- client/src/landing/AnonymousHome.tsx | 7 - client/src/utils/customHooks/useNow.hook.ts | 51 +++ client/src/utils/helpers/EnhancedState.ts | 6 + tests/cypress/e2e/maintenance.spec.ts | 40 +- 23 files changed, 1732 insertions(+), 137 deletions(-) create mode 100644 client/src/features/admin/IncidentsAndMaintenanceSection.tsx create mode 100644 client/src/features/platform/api/platform-empty.api.ts create mode 100644 client/src/features/platform/api/platform.api-config.ts create mode 100644 client/src/features/platform/api/platform.api.ts create mode 100644 client/src/features/platform/api/platform.generated-api.ts create mode 100644 client/src/features/platform/api/platform.openapi.json create mode 100644 client/src/features/platform/components/StatusBanner.tsx create mode 100644 client/src/features/platform/components/StatusPageIncidentUpdates.tsx create mode 100644 client/src/features/platform/components/StatusSummary.tsx create mode 100644 client/src/features/platform/statuspage-api/statuspage-empty.api.ts create mode 100644 client/src/features/platform/statuspage-api/statuspage.api.ts create mode 100644 client/src/features/platform/statuspage-api/statuspage.types.ts create mode 100644 client/src/utils/customHooks/useNow.hook.ts diff --git a/client/package.json b/client/package.json index 4150bedc6..4e4b99acc 100644 --- a/client/package.json +++ b/client/package.json @@ -22,10 +22,11 @@ "storybook-wait-server": "wait-on http://127.0.0.1:6006", "storybook-test": "test-storybook", "storybook-compile-and-test": "concurrently -k -s first -n 'BUILD,TEST' -c 'magenta,blue' 'npm run storybook-build && npm run storybook-start-server' 'npm run storybook-wait-server && npm run storybook-test'", - "generate-api": "npm run generate-api:dataServicesUser && npm run generate-api:namespaceV2 && npm run generate-api:projectV2 && npm run generate-api:searchV2", + "generate-api": "npm run generate-api:dataServicesUser && npm run generate-api:namespaceV2 && npm run generate-api:projectV2 && generate-api:platform && npm run generate-api:searchV2", "generate-api:dataServicesUser": "rtk-query-codegen-openapi src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts", "generate-api:namespaceV2": "rtk-query-codegen-openapi src/features/projectsV2/api/namespace.api-config.ts", "generate-api:projectV2": "rtk-query-codegen-openapi src/features/projectsV2/api/projectV2.api-config.ts", + "generate-api:platform": "rtk-query-codegen-openapi src/features/platform/api/platform.api-config.ts", "generate-api:searchV2": "rtk-query-codegen-openapi src/features/searchV2/api/searchV2.api-config.ts" }, "type": "module", diff --git a/client/src/components/navbar/AnonymousNavBar.tsx b/client/src/components/navbar/AnonymousNavBar.tsx index 15b7b8042..7d7b1c604 100644 --- a/client/src/components/navbar/AnonymousNavBar.tsx +++ b/client/src/components/navbar/AnonymousNavBar.tsx @@ -16,13 +16,16 @@ * limitations under the License. */ +import cx from "classnames"; import { useCallback, useState } from "react"; import { List, Search } from "react-bootstrap-icons"; import { Link } from "react-router-dom"; import { Collapse, Nav, NavItem, Navbar, NavbarToggler } from "reactstrap"; + +import StatusBanner from "../../features/platform/components/StatusBanner"; import { NavBarWarnings } from "../../landing/NavBarWarnings"; +import type { AppParams } from "../../utils/context/appParams.types"; import { Url } from "../../utils/helpers/url"; -import cx from "classnames"; import { RenkuNavLink } from "../RenkuNavLink"; import { RenkuToolbarHelpMenu, @@ -30,7 +33,6 @@ import { RenkuToolbarNotifications, } from "./NavBarItems"; import { RENKU_LOGO } from "./navbar.constans"; -import type { AppParams } from "../../utils/context/appParams.types"; interface AnonymousNavBarProps { model: unknown; @@ -112,6 +114,7 @@ export default function AnonymousNavBar({ + ); diff --git a/client/src/components/navbar/LoggedInNavBar.tsx b/client/src/components/navbar/LoggedInNavBar.tsx index 5ab287007..25108965a 100644 --- a/client/src/components/navbar/LoggedInNavBar.tsx +++ b/client/src/components/navbar/LoggedInNavBar.tsx @@ -19,10 +19,11 @@ import cx from "classnames"; import { useCallback, useState } from "react"; import { List, Search } from "react-bootstrap-icons"; -import { Link, useLocation } from "react-router-dom"; +import { Link } from "react-router-dom"; import { Collapse, Nav, NavItem, Navbar, NavbarToggler } from "reactstrap"; +import StatusBanner from "../../features/platform/components/StatusBanner"; import { NavBarWarnings } from "../../landing/NavBarWarnings"; -import { StatuspageBanner } from "../../statuspage"; +import { AppParams } from "../../utils/context/appParams.types"; import { Url } from "../../utils/helpers/url"; import { RenkuNavLink } from "../RenkuNavLink"; import { @@ -37,7 +38,7 @@ import { RENKU_LOGO } from "./navbar.constans"; interface LoggedInNavBarProps { model: unknown; notifications: unknown; - params: unknown; + params: AppParams; } export default function LoggedInNavBar({ @@ -45,9 +46,7 @@ export default function LoggedInNavBar({ notifications, params, }: LoggedInNavBarProps) { - const location = useLocation(); - - const uiShortSha = (params as { UI_SHORT_SHA: string }).UI_SHORT_SHA; + const uiShortSha = params.UI_SHORT_SHA; const [isOpen, setIsOpen] = useState(false); @@ -127,11 +126,7 @@ export default function LoggedInNavBar({ - + ); diff --git a/client/src/features/admin/AdminPage.tsx b/client/src/features/admin/AdminPage.tsx index 99f763b45..12c87b674 100644 --- a/client/src/features/admin/AdminPage.tsx +++ b/client/src/features/admin/AdminPage.tsx @@ -53,6 +53,7 @@ import AddResourceClassButton from "./AddResourceClassButton"; import AddResourcePoolButton from "./AddResourcePoolButton"; import AddUserToResourcePoolButton from "./AddUserToResourcePoolButton"; import DeleteResourceClassButton from "./DeleteResourceClassButton"; +import IncidentsAndMaintenanceSection from "./IncidentsAndMaintenanceSection"; import SessionEnvironmentsSection from "./SessionEnvironmentsSection"; import UpdateResourceClassButton from "./UpdateResourceClassButton"; import UpdateResourcePoolQuotaButton from "./UpdateResourcePoolQuotaButton"; @@ -66,6 +67,7 @@ export default function AdminPage() { return ( <>

Admin Panel

+ diff --git a/client/src/features/admin/IncidentsAndMaintenanceSection.tsx b/client/src/features/admin/IncidentsAndMaintenanceSection.tsx new file mode 100644 index 000000000..b4d733b09 --- /dev/null +++ b/client/src/features/admin/IncidentsAndMaintenanceSection.tsx @@ -0,0 +1,326 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +import { skipToken } from "@reduxjs/toolkit/query"; +import cx from "classnames"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { + BoxArrowUpRight, + CheckCircleFill, + XCircleFill, + XLg, +} from "react-bootstrap-icons"; +import { useForm } from "react-hook-form"; +import { Link } from "react-router-dom-v5-compat"; +import { + Alert, + Button, + Card, + CardBody, + CardHeader, + Collapse, + Form, + Label, + Nav, + NavItem, + TabContent, + TabPane, +} from "reactstrap"; + +import { Loader } from "../../components/Loader"; +import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; +import ChevronFlippedIcon from "../../components/icons/ChevronFlippedIcon"; +import LazyRenkuMarkdown from "../../components/markdown/LazyRenkuMarkdown"; +import { Docs } from "../../utils/constants/Docs"; +import AppContext from "../../utils/context/appContext"; +import { DEFAULT_APP_PARAMS } from "../../utils/context/appParams.constants"; +import { + useGetPlatformConfigQuery, + usePatchPlatformConfigMutation, +} from "../platform/api/platform.api"; +import { useGetSummaryQuery } from "../platform/statuspage-api/statuspage.api"; + +export default function IncidentsAndMaintenanceSection() { + const { params } = useContext(AppContext); + const statusPageId = + params?.STATUSPAGE_ID ?? DEFAULT_APP_PARAMS.STATUSPAGE_ID; + + return ( +
+

Incidents And Maintenance

+ +

+ + Renku documentation about incidents and maintenance + + +

+ + + + +
+ ); +} + +interface StatusPageCheckProps { + statusPageId: string; +} + +function StatusPageCheck({ statusPageId }: StatusPageCheckProps) { + const { + data: summary, + isLoading, + error, + } = useGetSummaryQuery(statusPageId ? { statusPageId } : skipToken); + + const statusPageManageUrl = `https://manage.statuspage.io/pages/${statusPageId}`; + + if (!statusPageId) { + return ( +

+ Status Page ID: Not configured +

+ ); + } + + const checkContent = isLoading ? ( +

+ + Checking status from statuspage.io... +

+ ) : error || summary == null ? ( + <> +

+ + Error: could not retrieve RenkuLab's status from statuspage.io. +

+ {error && } + + ) : ( +

+ + Status retrieved from{" "} + + {summary.page.url} + + + . +

+ ); + + return ( + <> +

+ Status Page ID:{" "} + + {statusPageId} + {" "} + (click to open the management page) +

+ {checkContent} + + ); +} + +function IncidentBannerSection() { + const { + data: platformConfig, + isLoading, + error, + } = useGetPlatformConfigQuery(); + + const [patchPlatformConfig, result] = usePatchPlatformConfigMutation(); + + const { + register, + formState: { isDirty }, + handleSubmit, + reset, + watch, + } = useForm({ + defaultValues: { incidentBanner: platformConfig?.incident_banner }, + }); + const incidentBanner = watch("incidentBanner"); + + const onSubmit = useCallback( + (data: IncidentBannerForm) => { + const incidentBanner = data.incidentBanner.trim(); + + patchPlatformConfig({ + "If-Match": platformConfig?.etag ?? "", + platformConfigPatch: { incident_banner: incidentBanner }, + }); + }, + [patchPlatformConfig, platformConfig?.etag] + ); + + const onClearIncidentBanner = useCallback( + () => onSubmit({ incidentBanner: "" }), + [onSubmit] + ); + + useEffect(() => { + if (result.isSuccess) { + reset({ incidentBanner: result.data.incident_banner }); + } + }, [reset, result.data?.incident_banner, result.isSuccess]); + + const [isOpen, setIsOpen] = useState(false); + const onToggleOpen = useCallback(() => setIsOpen((open) => !open), []); + + const [tab, setTab] = useState<"write-tab" | "preview-tab">("write-tab"); + const onClickWrite = useCallback(() => setTab("write-tab"), []); + const onClickPreview = useCallback(() => setTab("preview-tab"), []); + + if (isLoading) { + return ( +

+ + Loading platform configuration... +

+ ); + } + + if (error || !platformConfig) { + return ( +
+

Error: could not load platform configuration.

+ {error && } +
+ ); + } + + return ( + + + + + + +
+
+ + + + +