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 ( + + + + + + +
+
+ + + + +