This is an automated email from the ASF dual-hosted git repository. porcelli pushed a commit to branch KOGITO-8015-feature-preview in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools-temporary-rnd-do-not-use.git
commit db67ee42b83b2a09a5110d66c1c94c7b9da62366 Author: Fabrizio Antonangeli <[email protected]> AuthorDate: Wed Jan 25 20:08:55 2023 +0100 KOGITO-8323: Refactor Settings according to UX redesign (#1404) * KOGITO-8147 Refactor the app bar and menu according to UX redesign * refactor to function style * remove new home page * remove old home page * Updated OnlineEditorPage.tsx * Add Settings routes and Settings Nav sidebar * GitHubSettings page wip * Rename packages/serverless-logic-sandbox/src/settings/github/GitHubSettingsTab.tsx -> packages/serverless-logic-sandbox/src/settings/github/GitHubSettings.tsx * GitHubSettings page * Removed quickstart link after branching * add KieSandboxExtendedServicesSettings route * Fix expand content when sidebar is collapsed * Moved new settings routes to src/newSettings * Completing the new settings folder * WIP OpenShiftSettings * OpenShiftSettings page * Fix "setup" modal hidden by "add connection" modal * Fix: Can't perform a React state update on an unmounted component * Mv ServiceAccountSettingsTab -> ServiceAccountSettings * ServiceAccountSettings * R packages/serverless-logic-sandbox/src/newSettings/serviceRegistry/ServiceRegistrySettingsTab.tsx -> packages/serverless-logic-sandbox/src/newSettings/serviceRegistry/ServiceRegistrySettings.tsx * Refactor ServiceRegistrySettings * R packages/serverless-logic-sandbox/src/newSettings/kafka/ApacheKafkaSettingsTab.tsx -> packages/serverless-logic-sandbox/src/newSettings/kafka/ApacheKafkaSettings.tsx * Refactor ApacheKafkaSettings * R packages/serverless-logic-sandbox/src/newSettings/featurePreview/FeaturePreviewSettingsTab.tsx -> packages/serverless-logic-sandbox/src/newSettings/featurePreview/FeaturePreviewSettings.tsx * Refactor FeaturePreviewSettings * Sidebar title same as OpenShift Console * deleted: packages/serverless-logic-sandbox/src/newSettings/SettingsModalBody.tsx removed settings modal related code * Fix Copyright * Add check not to show the loading in modal if we are in settings route * Removed unnecessary code for input focus * Code review * Fixed issues after merge conflicts * Fix issue after merge Co-authored-by: Xaiofeng Bai <[email protected]> --- packages/serverless-logic-web-tools/src/App.tsx | 2 + .../src/homepage/pageTemplate/OnlineEditorPage.tsx | 16 +- .../src/navigation/Routes.ts | 12 + .../src/navigation/RoutesSwitch.tsx | 6 +- .../src/newSettings/SettingsButton.tsx | 35 +++ .../{settings => newSettings}/SettingsContext.tsx | 73 +---- .../KieSandboxExtendedServicesSettings.tsx | 273 ++++++++++++++++++ .../featurePreview/FeaturePreviewConfig.tsx | 42 +++ .../featurePreview/FeaturePreviewSettings.tsx | 72 +++++ .../src/newSettings/github/GitHubSettings.tsx | 239 +++++++++++++++ .../src/newSettings/github/Hooks.tsx | 34 +++ .../src/newSettings/kafka/ApacheKafkaSettings.tsx | 297 +++++++++++++++++++ .../src/newSettings/kafka/KafkaSettingsConfig.tsx | 70 +++++ .../newSettings/openshift/OpenShiftSettings.tsx | 151 ++++++++++ .../openshift/OpenShiftSettingsConfig.tsx | 58 ++++ .../openshift/OpenShiftSettingsSimpleConfig.tsx | 319 +++++++++++++++++++++ .../src/newSettings/routes/SettingsPageRoutes.tsx | 59 ++++ .../serviceAccount/ServiceAccountConfig.tsx | 66 +++++ .../serviceAccount/ServiceAccountSettings.tsx | 307 ++++++++++++++++++++ .../serviceRegistry/ServiceRegistryConfig.tsx | 66 +++++ .../serviceRegistry/ServiceRegistrySettings.tsx | 305 ++++++++++++++++++++ .../src/newSettings/uiNav/SettingsPageNav.tsx | 75 +++++ .../src/settings/SettingsContext.tsx | 14 +- .../static/resources/style.css | 12 + 24 files changed, 2533 insertions(+), 70 deletions(-) diff --git a/packages/serverless-logic-web-tools/src/App.tsx b/packages/serverless-logic-web-tools/src/App.tsx index 540822c69c..eb5d51eb35 100644 --- a/packages/serverless-logic-web-tools/src/App.tsx +++ b/packages/serverless-logic-web-tools/src/App.tsx @@ -24,6 +24,7 @@ import { NavigationContextProvider } from "./navigation/NavigationContextProvide import { RoutesSwitch } from "./navigation/RoutesSwitch"; import { OpenShiftContextProvider } from "./openshift/OpenShiftContextProvider"; import { SettingsContextProvider } from "./settings/SettingsContext"; +import { SettingsContextProvider as NewSettingsContextProvider } from "./newSettings/SettingsContext"; import { VirtualServiceRegistryContextProvider } from "./virtualServiceRegistry/VirtualServiceRegistryContextProvider"; import { WorkspacesContextProvider } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContextProvider"; @@ -35,6 +36,7 @@ export const App = () => ( [EnvContextProvider, {}], [KieSandboxExtendedServicesContextProvider, {}], [SettingsContextProvider, {}], + [NewSettingsContextProvider, {}], [WorkspacesContextProvider, { workspacesSharedWorkerScriptUrl: "workspace/worker/sharedWorker.js" }], [OpenShiftContextProvider, {}], [VirtualServiceRegistryContextProvider, {}], diff --git a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx index 6f13f1ae49..7f55b5605a 100644 --- a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx @@ -32,7 +32,7 @@ import { SkipToContent, } from "@patternfly/react-core"; import { Page } from "@patternfly/react-core/dist/js/components/Page"; -import { useHistory } from "react-router"; +import { useHistory, useRouteMatch } from "react-router"; import { KieSandboxExtendedServicesIcon } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesIcon"; import { useRoutes } from "../../navigation/Hooks"; import { OpenshiftDeploymentsDropdown } from "../../openshift/dropdown/OpenshiftDeploymentsDropdown"; @@ -40,6 +40,7 @@ import { SettingsButton } from "../../settings/SettingsButton"; import { HomePageNav } from "../uiNav/HomePageNav"; import { useLocation } from "react-router-dom"; import { useState } from "react"; +import { SettingsPageNav } from "../../newSettings/uiNav/SettingsPageNav"; import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; import { ExclamationIcon, BarsIcon } from "@patternfly/react-icons/dist/js/icons"; @@ -47,6 +48,7 @@ export function OnlineEditorPage(props: { children?: React.ReactNode }) { const history = useHistory(); const routes = useRoutes(); const [isNavOpen, setIsNavOpen] = useState(true); + const isRouteInSettingsSection = useRouteMatch(routes.settings.home.path({})); const navToggle = () => { setIsNavOpen(!isNavOpen); }; @@ -127,9 +129,17 @@ export function OnlineEditorPage(props: { children?: React.ReactNode }) { ); const location = useLocation(); - const sidebar = ( - <PageSidebar nav={<HomePageNav pathname={location.pathname}></HomePageNav>} isNavOpen={isNavOpen} theme="dark" /> + const pageNav = useMemo( + () => + !isRouteInSettingsSection ? ( + <HomePageNav pathname={location.pathname}></HomePageNav> + ) : ( + <SettingsPageNav pathname={location.pathname}></SettingsPageNav> + ), + [location, isRouteInSettingsSection] ); + + const sidebar = <PageSidebar nav={pageNav} isNavOpen={isNavOpen} theme="dark" />; const mainContainerId = "main-content-page-layout-tertiary-nav"; const pageSkipToContent = <SkipToContent href={`#${mainContainerId}`}>Skip to content</SkipToContent>; diff --git a/packages/serverless-logic-web-tools/src/navigation/Routes.ts b/packages/serverless-logic-web-tools/src/navigation/Routes.ts index 54189fd1c6..b62375e791 100644 --- a/packages/serverless-logic-web-tools/src/navigation/Routes.ts +++ b/packages/serverless-logic-web-tools/src/navigation/Routes.ts @@ -15,6 +15,7 @@ */ const IS_HASH_ROUTER = true; +const SETTINGS_ROUTE = "/settings"; export enum QueryParams { SETTINGS = "settings", @@ -128,6 +129,17 @@ export const routes = { `/${workspaceId}/file/${fileRelativePath}${extension ? "." + extension : ""}` ), + settings: { + home: new Route<{}>(() => SETTINGS_ROUTE), + github: new Route<{}>(() => `${SETTINGS_ROUTE}/github`), + openshift: new Route<{}>(() => `${SETTINGS_ROUTE}/openshift`), + kie_sandbox_extended_services: new Route<{}>(() => `${SETTINGS_ROUTE}/kie_sandbox_extended_services`), + service_account: new Route<{}>(() => `${SETTINGS_ROUTE}/serviceAccount`), + service_registry: new Route<{}>(() => `${SETTINGS_ROUTE}/serviceRegistry`), + kafka: new Route<{}>(() => `${SETTINGS_ROUTE}/kafka`), + feature_preview: new Route<{}>(() => `${SETTINGS_ROUTE}/featurePreview`), + }, + static: { sample: new Route<{ pathParams: "type" | "name" }>(({ type, name }) => `samples/${name}/${name}.${type}`), images: { diff --git a/packages/serverless-logic-web-tools/src/navigation/RoutesSwitch.tsx b/packages/serverless-logic-web-tools/src/navigation/RoutesSwitch.tsx index 686c59fd1b..004b9837b9 100644 --- a/packages/serverless-logic-web-tools/src/navigation/RoutesSwitch.tsx +++ b/packages/serverless-logic-web-tools/src/navigation/RoutesSwitch.tsx @@ -16,14 +16,16 @@ import * as React from "react"; import { useMemo } from "react"; -import { Route, Switch } from "react-router-dom"; +import { Route, Switch, useRouteMatch } from "react-router-dom"; import { useRoutes } from "./Hooks"; import { OnlineEditorPage } from "../homepage/pageTemplate/OnlineEditorPage"; import { Label } from "@patternfly/react-core/dist/js/components/Label"; import { HomePageRoutes } from "../homepage/routes/HomePageRoutes"; +import { SettingsPageRoutes } from "../newSettings/routes/SettingsPageRoutes"; export function RoutesSwitch() { const routes = useRoutes(); + const isRouteInSettingsSection = useRouteMatch(routes.settings.home.path({})); const buildInfo = useMemo(() => { return process.env["WEBPACK_REPLACE__buildInfo"]; }, []); @@ -44,7 +46,7 @@ export function RoutesSwitch() { }) => { return ( <OnlineEditorPage> - <HomePageRoutes /> + {!isRouteInSettingsSection ? <HomePageRoutes /> : <SettingsPageRoutes />} {buildInfo && ( <div className={"kie-tools--build-info"}> <Label>{buildInfo}</Label> diff --git a/packages/serverless-logic-web-tools/src/newSettings/SettingsButton.tsx b/packages/serverless-logic-web-tools/src/newSettings/SettingsButton.tsx new file mode 100644 index 0000000000..7b1cde4e00 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/SettingsButton.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * 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 * as React from "react"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { CogIcon } from "@patternfly/react-icons/dist/js/icons/cog-icon"; +import { useSettingsDispatch } from "./SettingsContext"; +import { Link } from "react-router-dom"; +import { useRoutes } from "../navigation/Hooks"; + +export function SettingsButton() { + const settingsDispatch = useSettingsDispatch(); + const routes = useRoutes(); + + return ( + <Link to={routes.settings.home.path({})}> + <Button variant={ButtonVariant.plain} aria-label="Settings" className={"kie-tools--masthead-hoverable-dark"}> + <CogIcon /> + </Button> + </Link> + ); +} diff --git a/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx b/packages/serverless-logic-web-tools/src/newSettings/SettingsContext.tsx similarity index 84% copy from packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx copy to packages/serverless-logic-web-tools/src/newSettings/SettingsContext.tsx index 242e71c961..76a2eeb0bf 100644 --- a/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx +++ b/packages/serverless-logic-web-tools/src/newSettings/SettingsContext.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * Copyright 2023 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,30 +14,25 @@ * limitations under the License. */ +import { OpenShiftConnection } from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; +import { OpenShiftService } from "@kie-tools-core/openshift/dist/service/OpenShiftService"; +import { Octokit } from "@octokit/rest"; import * as React from "react"; -import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { getCookie, setCookie } from "../cookies"; -import { Octokit } from "@octokit/rest"; -import { useQueryParams } from "../queryParams/QueryParamsContext"; -import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal"; -import { SettingsModalBody, SettingsTabs } from "./SettingsModalBody"; -import { readOpenShiftConfigCookie } from "./openshift/OpenShiftSettingsConfig"; -import { OpenShiftConnection } from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; +import { SwfServiceCatalogStore } from "../editor/api/SwfServiceCatalogStore"; +import { useKieSandboxExtendedServices } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; import { OpenShiftInstanceStatus } from "../openshift/OpenShiftInstanceStatus"; -import { OpenShiftService } from "@kie-tools-core/openshift/dist/service/OpenShiftService"; -import { useHistory } from "react-router"; -import { QueryParams } from "../navigation/Routes"; -import { GITHUB_AUTH_TOKEN_COOKIE_NAME } from "./github/GitHubSettingsTab"; +import { FeaturePreviewSettingsConfig, readFeaturePreviewConfigCookie } from "./featurePreview/FeaturePreviewConfig"; +import { GITHUB_AUTH_TOKEN_COOKIE_NAME } from "./github/GitHubSettings"; import { KafkaSettingsConfig, readKafkaConfigCookie } from "./kafka/KafkaSettingsConfig"; +import { readOpenShiftConfigCookie } from "./openshift/OpenShiftSettingsConfig"; import { readServiceAccountConfigCookie, ServiceAccountSettingsConfig } from "./serviceAccount/ServiceAccountConfig"; import { readServiceRegistryConfigCookie, ServiceRegistrySettingsConfig, } from "./serviceRegistry/ServiceRegistryConfig"; -import { useKieSandboxExtendedServices } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; -import { KieSandboxExtendedServicesStatus } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; -import { SwfServiceCatalogStore } from "../editor/api/SwfServiceCatalogStore"; -import { FeaturePreviewSettingsConfig, readFeaturePreviewConfigCookie } from "./featurePreview/FeaturePreviewConfig"; export enum AuthStatus { SIGNED_OUT, @@ -69,8 +64,6 @@ export class ExtendedServicesConfig { } export interface SettingsContextType { - isOpen: boolean; - activeTab: SettingsTabs; openshift: { status: OpenShiftInstanceStatus; config: OpenShiftConnection; @@ -99,8 +92,6 @@ export interface SettingsContextType { } export interface SettingsDispatchContextType { - open: (activeTab?: SettingsTabs) => void; - close: () => void; openshift: { service: OpenShiftService; setStatus: React.Dispatch<React.SetStateAction<OpenShiftInstanceStatus>>; @@ -132,31 +123,6 @@ export const SettingsContext = React.createContext<SettingsContextType>({} as an export const SettingsDispatchContext = React.createContext<SettingsDispatchContextType>({} as any); export function SettingsContextProvider(props: any) { - const queryParams = useQueryParams(); - const history = useHistory(); - const [isOpen, setOpen] = useState(false); - const [activeTab, setActiveTab] = useState(SettingsTabs.GITHUB); - - useEffect(() => { - setOpen(queryParams.has(QueryParams.SETTINGS)); - setActiveTab((queryParams.get(QueryParams.SETTINGS) as SettingsTabs) ?? SettingsTabs.GITHUB); - }, [queryParams]); - - const open = useCallback( - (activeTab = SettingsTabs.GITHUB) => { - history.replace({ - search: queryParams.with(QueryParams.SETTINGS, activeTab).toString(), - }); - }, - [history, queryParams] - ); - - const close = useCallback(() => { - history.replace({ - search: queryParams.without(QueryParams.SETTINGS).toString(), - }); - }, [history, queryParams]); - //github const [githubAuthStatus, setGitHubAuthStatus] = useState(AuthStatus.LOADING); const [githubOctokit, setGitHubOctokit] = useState<Octokit>(new Octokit()); @@ -256,8 +222,6 @@ export function SettingsContextProvider(props: any) { const dispatch = useMemo(() => { return { - open, - close, openshift: { service: openshiftService, setStatus: setOpenshiftStatus, @@ -285,10 +249,8 @@ export function SettingsContextProvider(props: any) { }, }; }, [ - close, githubAuthService, githubOctokit, - open, openshiftService, kieSandboxExtendedServices.saveNewConfig, serviceCatalogStore, @@ -296,8 +258,6 @@ export function SettingsContextProvider(props: any) { const value = useMemo(() => { return { - isOpen, - activeTab, openshift: { status: openshiftStatus, config: openshiftConfig, @@ -325,8 +285,6 @@ export function SettingsContextProvider(props: any) { }, }; }, [ - isOpen, - activeTab, openshiftStatus, openshiftConfig, githubAuthStatus, @@ -342,14 +300,7 @@ export function SettingsContextProvider(props: any) { return ( <SettingsContext.Provider value={value}> - <SettingsDispatchContext.Provider value={dispatch}> - {githubAuthStatus !== AuthStatus.LOADING && <>{props.children}</>} - <Modal title="Settings" isOpen={isOpen} onClose={close} variant={ModalVariant.large}> - <div style={{ height: "calc(100vh * 0.5)" }} className={"kie-tools--settings-modal-content"}> - <SettingsModalBody /> - </div> - </Modal> - </SettingsDispatchContext.Provider> + <SettingsDispatchContext.Provider value={dispatch}>{props.children}</SettingsDispatchContext.Provider> </SettingsContext.Provider> ); } diff --git a/packages/serverless-logic-web-tools/src/newSettings/extendedServices/KieSandboxExtendedServicesSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/extendedServices/KieSandboxExtendedServicesSettings.tsx new file mode 100644 index 0000000000..4e163afd88 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/extendedServices/KieSandboxExtendedServicesSettings.tsx @@ -0,0 +1,273 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Modal, ModalVariant } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { ActionGroup, Form, FormAlert, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon"; +import * as React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { ExtendedServicesConfig, useSettings, useSettingsDispatch } from "../../settings/SettingsContext"; + +export function KieSandboxExtendedServicesSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const [config, setConfig] = useState(settings.kieSandboxExtendedServices.config); + const [isModalOpen, setIsModalOpen] = useState(false); + + const isCurrentConfigValid = useMemo( + () => config.host.trim().length > 0 && config.buildUrl().trim().length > 0, + [config] + ); + + const onClearHost = useCallback(() => setConfig(new ExtendedServicesConfig("", config.port)), [config]); + const onClearPort = useCallback(() => setConfig(new ExtendedServicesConfig(config.host, "")), [config]); + + const onHostChanged = useCallback( + (newValue: string) => setConfig(new ExtendedServicesConfig(newValue, config.port)), + [config] + ); + const onPortChanged = useCallback( + (newValue: string) => setConfig(new ExtendedServicesConfig(config.host, newValue)), + [config] + ); + + const onConnect = useCallback(() => { + settingsDispatch.kieSandboxExtendedServices.setConfig(config); + }, [settingsDispatch.kieSandboxExtendedServices, config]); + + const onReset = useCallback(() => { + const emptyConfig = new ExtendedServicesConfig("", ""); + setConfig(emptyConfig); + settingsDispatch.kieSandboxExtendedServices.setConfig(emptyConfig); + }, [settingsDispatch.kieSandboxExtendedServices]); + + const handleModalToggle = useCallback(() => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>KIE Sandbox Extended Services</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for proxying Serverless Logic Web Tools requests to OpenShift, thus + making it possible to deploy models. + <br /> All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection isFilled> + <PageSection variant={"light"}> + {kieSandboxExtendedServices.status === KieSandboxExtendedServicesStatus.RUNNING ? ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>You are connect to the KIE Sandbox Extended Services.</Text> + </TextContent> + <EmptyStateBody> + Deploying models is <b>enabled</b>. + <br /> + <b>URL: </b> + <i>{config.buildUrl()}</i> + <br /> + <br /> + <Button variant={ButtonVariant.secondary} onClick={onReset}> + Reset + </Button> + </EmptyStateBody> + </EmptyState> + ) : ( + <> + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>You are not connected to KIE Sandbox Extended Services.</Text> + </TextContent> + <EmptyStateBody> + You currently have no KIE Sandbox Extended Services connections.{" "} + <a + onClick={() => { + kieSandboxExtendedServices.setInstallTriggeredBy(undefined); + kieSandboxExtendedServices.setModalOpen(true); + }} + > + Click to setup + </a> + <br /> + <br /> + <Button + variant={ButtonVariant.primary} + onClick={handleModalToggle} + data-testid="add-connection-button" + > + Add connection + </Button> + </EmptyStateBody> + </EmptyState> + </> + )} + </PageSection> + </PageSection> + + <Modal + title="Add connection" + isOpen={ + isModalOpen && + kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.RUNNING && + !kieSandboxExtendedServices.isModalOpen + } + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <Form> + <FormAlert> + <Alert + variant="danger" + title={ + <Text> + You are not connected to KIE Sandbox Extended Services.{" "} + <a + onClick={() => { + kieSandboxExtendedServices.setInstallTriggeredBy(undefined); + kieSandboxExtendedServices.setModalOpen(true); + }} + > + Click to setup + </a> + </Text> + } + aria-live="polite" + isInline + /> + </FormAlert> + <PageSection variant={"light"} isFilled={true} style={{ height: "100%" }}> + <FormGroup + label={"Host"} + labelIcon={ + <Popover bodyContent={"The host associated with the KIE Sandbox Extended Services URL instance."}> + <button + type="button" + aria-label="More info for host field" + onClick={(e) => e.preventDefault()} + aria-describedby="host-server-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="host-server-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="host-server-field" + name="host-server-field" + aria-label="Host field" + aria-describedby="host-server-field-helper" + value={config.host} + onChange={onHostChanged} + tabIndex={1} + data-testid="host-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear host button" onClick={onClearHost}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={"Port"} + labelIcon={ + <Popover + bodyContent={"The port number associated with the KIE Sandbox Extended Services URL instance."} + > + <button + type="button" + aria-label="More info for port field" + onClick={(e) => e.preventDefault()} + aria-describedby="port-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="port-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="port-field" + name="port-field" + aria-label="Port field" + aria-describedby="port-field-helper" + value={config.port} + onChange={onPortChanged} + tabIndex={2} + data-testid="port-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear port button" onClick={onClearPort}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <ActionGroup> + <Button + isDisabled={!isCurrentConfigValid} + id="kie-sandbox-extended-services-config-connect-button" + key="connect" + variant="primary" + onClick={onConnect} + data-testid="connect-config-button" + > + Connect + </Button> + <Button key="cancel" variant="link" onClick={handleModalToggle} data-testid="connect-cancel-button"> + Connect + </Button> + </ActionGroup> + </PageSection> + </Form> + </Modal> + </Page> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewConfig.tsx new file mode 100644 index 0000000000..36cce4c180 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewConfig.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { getCookie, makeCookieName, setCookie } from "../../cookies"; + +export const FEATURE_PREVIEW_STUNNER_ENABLED_COOKIE_NAME = makeCookieName("feature-preview", "stunner-enabled"); + +export interface FeaturePreviewSettingsConfig { + stunnerEnabled: boolean; +} + +export const DEFAULT_CONFIG: FeaturePreviewSettingsConfig = { + stunnerEnabled: true, +}; + +export function readFeaturePreviewConfigCookie(): FeaturePreviewSettingsConfig { + const stunnerEnabledCookie = getCookie(FEATURE_PREVIEW_STUNNER_ENABLED_COOKIE_NAME); + return { + stunnerEnabled: stunnerEnabledCookie ? stunnerEnabledCookie === "true" : DEFAULT_CONFIG.stunnerEnabled, + }; +} + +export function saveStunnerEnabledCookie(isEnabled: boolean): void { + setCookie(FEATURE_PREVIEW_STUNNER_ENABLED_COOKIE_NAME, String(isEnabled)); +} + +export function saveConfigCookie(config: FeaturePreviewSettingsConfig): void { + saveStunnerEnabledCookie(config.stunnerEnabled); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewSettings.tsx new file mode 100644 index 0000000000..4b28dc3182 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/featurePreview/FeaturePreviewSettings.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates. + * + * 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 * as React from "react"; +import { Form } from "@patternfly/react-core/dist/js/components/Form"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { useCallback, useEffect, useState } from "react"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { Checkbox } from "@patternfly/react-core/dist/js/components/Checkbox"; +import { saveConfigCookie } from "./FeaturePreviewConfig"; + +export function FeaturePreviewSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [config, setConfig] = useState(settings.featurePreview.config); + const [stunnerEnabled, setStunnerEnabled] = useState(config.stunnerEnabled); + + useEffect(() => { + settingsDispatch.featurePreview.setConfig(config); + saveConfigCookie(config); + }, [config, settingsDispatch.featurePreview]); + + const onStunnerEnabledChanged = useCallback( + (isEnabled: boolean) => { + setStunnerEnabled(isEnabled); + setConfig({ ...config, stunnerEnabled: isEnabled }); + }, + [config] + ); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>Feature Preview</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for configuring the preview of features that are not fully supported yet. + <br /> All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection> + <PageSection variant={"light"}> + <Form> + <Checkbox + id="feature-preview-enable-stunner" + label="Kogito Serverless Workflow Visualization" + description={"Enable/disable Kogito Serverless Workflow Visualization for JSON files."} + isChecked={stunnerEnabled} + onChange={onStunnerEnabledChanged} + /> + </Form> + </PageSection> + </PageSection> + </Page> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/github/GitHubSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/github/GitHubSettings.tsx new file mode 100644 index 0000000000..bd3be0578f --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/github/GitHubSettings.tsx @@ -0,0 +1,239 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Modal, ModalVariant } from "@patternfly/react-core"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { Form, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import { ExclamationTriangleIcon } from "@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon"; +import { ExternalLinkAltIcon } from "@patternfly/react-icons/dist/js/icons/external-link-alt-icon"; +import { GithubIcon } from "@patternfly/react-icons/dist/js/icons/github-icon"; +import * as React from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { makeCookieName } from "../../cookies"; +import { AuthStatus, useSettings, useSettingsDispatch } from "../../settings/SettingsContext"; + +export const GITHUB_OAUTH_TOKEN_SIZE = 40; +export const GITHUB_TOKENS_URL = "https://github.com/settings/tokens"; +export const GITHUB_TOKENS_HOW_TO_URL = + "https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line"; +export const GITHUB_AUTH_TOKEN_COOKIE_NAME = makeCookieName("github", "oauth-token"); + +export enum GitHubSignInOption { + PERSONAL_ACCESS_TOKEN, + OAUTH, +} + +enum GitHubTokenScope { + GIST = "gist", + REPO = "repo", +} + +export function GitHubSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + + const [potentialGitHubToken, setPotentialGitHubToken] = useState<string | undefined>(undefined); + const [isGitHubTokenValid, setIsGitHubTokenValid] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const tokenInput = useRef<HTMLInputElement>(null); + + const githubTokenValidated = useMemo(() => { + return isGitHubTokenValid ? "default" : "error"; + }, [isGitHubTokenValid]); + + const githubTokenHelperText = useMemo(() => { + return isGitHubTokenValid ? undefined : "Invalid token. Check if it has the 'repo' scope."; + }, [isGitHubTokenValid]); + + const githubTokenToDisplay = useMemo(() => { + return obfuscate(potentialGitHubToken ?? settings.github.token) ?? ""; + }, [settings.github, potentialGitHubToken]); + + const handleModalToggle = useCallback(() => { + setPotentialGitHubToken(undefined); + setIsGitHubTokenValid(true); + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + const onPasteGitHubToken = useCallback( + (e) => { + const token = e.clipboardData.getData("text/plain").slice(0, GITHUB_OAUTH_TOKEN_SIZE); + setPotentialGitHubToken(token); + settingsDispatch.github.authService + .authenticate(token) + .then(() => handleModalToggle()) + .catch(() => setIsGitHubTokenValid(false)); + }, + [settingsDispatch.github.authService, handleModalToggle] + ); + + const onSignOutFromGitHub = useCallback(() => { + settingsDispatch.github.authService.reset(); + setPotentialGitHubToken(undefined); + }, [settingsDispatch.github.authService]); + + return ( + <Page> + <PageSection variant={"light"}> + <TextContent> + <Text component={TextVariants.h1}>GitHub</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for creating repositories containing models you design, and syncing + changes with GitHub. + <br /> + All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection isFilled> + <PageSection variant={"light"}> + {settings.github.authStatus === AuthStatus.TOKEN_EXPIRED && ( + <EmptyState> + <EmptyStateIcon icon={ExclamationTriangleIcon} /> + <TextContent> + <Text component={"h2"}>GitHub Token expired</Text> + </TextContent> + <EmptyStateBody> + <TextContent>Reset your token to sign in with GitHub again.</TextContent> + </EmptyStateBody> + <br /> + <Button variant={ButtonVariant.secondary} onClick={onSignOutFromGitHub}> + Reset + </Button> + </EmptyState> + )} + {settings.github.authStatus === AuthStatus.LOADING && ( + <EmptyState> + <EmptyStateIcon icon={GithubIcon} /> + <TextContent> + <Text component={"h2"}>Signing in with GitHub</Text> + </TextContent> + <br /> + <br /> + <Spinner /> + </EmptyState> + )} + {settings.github.authStatus === AuthStatus.SIGNED_IN && ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>{"You're signed in with GitHub."}</Text> + </TextContent> + <EmptyStateBody> + Gists are <b>{settings.github.scopes?.includes(GitHubTokenScope.GIST) ? "enabled" : "disabled"}.</b> + <br /> + Private repositories are{" "} + <b>{settings.github.scopes?.includes(GitHubTokenScope.REPO) ? "enabled" : "disabled"}.</b> + <br /> + <b>Token: </b> + <i>{obfuscate(settings.github.token)}</i> + <br /> + <b>User: </b> + <i>{settings.github.user?.login}</i> + <br /> + <b>Scope: </b> + <i>{settings.github.scopes?.join(", ") || "(none)"}</i> + </EmptyStateBody> + <br /> + <Button variant={ButtonVariant.secondary} onClick={onSignOutFromGitHub}> + Sign out + </Button> + </EmptyState> + )} + {settings.github.authStatus === AuthStatus.SIGNED_OUT && ( + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>{"No access token"}</Text> + </TextContent> + <EmptyStateBody> + You currently have no tokens to display. Access tokens allow you for creating repositories containing + models you design, and syncing changes with GitHub. + </EmptyStateBody> + <Button variant={ButtonVariant.primary} onClick={handleModalToggle}> + Add access token + </Button> + </EmptyState> + )} + </PageSection> + </PageSection> + + <Modal + title="Create new token" + isOpen={isModalOpen && settings.github.authStatus !== AuthStatus.LOADING} + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <Form onSubmit={(e) => e.preventDefault()}> + <h3> + <a href={GITHUB_TOKENS_URL} target={"_blank"} rel="noopener noreferrer"> + Create a new token + <ExternalLinkAltIcon /> + </a> + </h3> + <FormGroup + isRequired={true} + helperTextInvalid={githubTokenHelperText} + validated={githubTokenValidated} + label={"Token"} + fieldId={"github-pat"} + helperText={"Your token must include the 'repo' scope."} + > + <InputGroup> + <TextInput + ref={tokenInput} + autoComplete={"off"} + id="token-input" + name="tokenInput" + aria-describedby="token-text-input-helper" + placeholder={"Paste your GitHub token here"} + maxLength={GITHUB_OAUTH_TOKEN_SIZE} + validated={githubTokenValidated} + value={githubTokenToDisplay} + onPaste={onPasteGitHubToken} + tabIndex={1} + /> + </InputGroup> + </FormGroup> + <br /> + </Form> + </Modal> + </Page> + ); +} + +export function obfuscate(token?: string) { + if (!token) { + return undefined; + } + + if (token.length <= 8) { + return token; + } + + const stars = new Array(token.length - 8).join("*"); + const pieceToObfuscate = token.substring(4, token.length - 4); + return token.replace(pieceToObfuscate, stars); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/github/Hooks.tsx b/packages/serverless-logic-web-tools/src/newSettings/github/Hooks.tsx new file mode 100644 index 0000000000..f05fde5cd4 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/github/Hooks.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * 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 { useMemo } from "react"; +import { AuthStatus, useSettings } from "../SettingsContext"; + +export function useGitHubAuthInfo() { + const settings = useSettings(); + return useMemo(() => { + if (settings.github.authStatus !== AuthStatus.SIGNED_IN) { + return undefined; + } + + return { + name: settings.github.user!.name, + email: settings.github.user!.email, + username: settings.github.user!.login, + password: settings.github.token!, + }; + }, [settings.github]); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/kafka/ApacheKafkaSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/kafka/ApacheKafkaSettings.tsx new file mode 100644 index 0000000000..7df8cb63dd --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/kafka/ApacheKafkaSettings.tsx @@ -0,0 +1,297 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Modal, ModalVariant } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { ActionGroup, Form, FormAlert, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon"; +import * as React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { EMPTY_CONFIG, isKafkaConfigValid, resetConfigCookie, saveConfigCookie } from "./KafkaSettingsConfig"; + +export function ApacheKafkaSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [config, setConfig] = useState(settings.apacheKafka.config); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalToggle = useCallback(() => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + const isExtendedServicesRunning = useMemo( + () => kieSandboxExtendedServices.status === KieSandboxExtendedServicesStatus.RUNNING, + [kieSandboxExtendedServices.status] + ); + + const isStoredConfigValid = useMemo( + () => isExtendedServicesRunning && isKafkaConfigValid(settings.apacheKafka.config), + [isExtendedServicesRunning, settings.apacheKafka.config] + ); + + const isCurrentConfigValid = useMemo( + () => isExtendedServicesRunning && isKafkaConfigValid(config), + [isExtendedServicesRunning, config] + ); + + const onClearBootstraServer = useCallback(() => setConfig({ ...config, bootstrapServer: "" }), [config]); + const onClearTopic = useCallback(() => setConfig({ ...config, topic: "" }), [config]); + + const onBootstrapServerChanged = useCallback( + (newValue: string) => setConfig({ ...config, bootstrapServer: newValue }), + [config] + ); + + const onTopicChanged = useCallback((newValue: string) => setConfig({ ...config, topic: newValue }), [config]); + + const onReset = useCallback(() => { + setConfig(EMPTY_CONFIG); + settingsDispatch.apacheKafka.setConfig(EMPTY_CONFIG); + resetConfigCookie(); + }, [settingsDispatch.apacheKafka]); + + const onApply = useCallback(() => { + settingsDispatch.apacheKafka.setConfig(config); + saveConfigCookie(config); + }, [config, settingsDispatch.apacheKafka]); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>Streams for Apache Kafka</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for connecting serverless deployments with your Streams for Apache Kafka + instance through a KafkaSource. + <br /> All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection> + {!isExtendedServicesRunning && ( + <> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link>{" "} + before configuring your Streams for Apache Kafka instance + </Text> + } + aria-live="polite" + isInline + > + KIE Sandbox Extended Services is necessary for connecting serverless deployments with your Streams for + Apache Kafka instance through a KafkaSource. + </Alert> + <br /> + </> + )} + <PageSection variant={"light"}> + {isStoredConfigValid ? ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>{"Your Streams for Apache Kafka information is set."}</Text> + </TextContent> + <EmptyStateBody> + Deploying models with a KafkaSource attached to the service is <b>enabled</b>. + <br /> + <b>Bootstrap server: </b> + <i>{config.bootstrapServer}</i> + <br /> + <b>Topic: </b> + <i>{config.topic}</i> + <br /> + <br /> + <Button variant={ButtonVariant.tertiary} onClick={onReset}> + Reset + </Button> + </EmptyStateBody> + </EmptyState> + ) : ( + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>No Streams for Apache Kafka information yet</Text> + </TextContent> + <EmptyStateBody> + To get started, add a Streams for Apache Kafka information. + <br /> + <br /> + <Button variant={ButtonVariant.primary} onClick={handleModalToggle} data-testid="add-connection-button"> + Add Streams for Apache Kafka + </Button> + </EmptyStateBody> + </EmptyState> + )} + </PageSection> + </PageSection> + + <Modal + title="Add Streams for Apache Kafka" + isOpen={ + isModalOpen && + kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.STOPPED && + !isStoredConfigValid + } + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <Form> + {!isExtendedServicesRunning && ( + <FormAlert> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}> + KIE Sandbox Extended Services + </Link>{" "} + before configuring your Streams for Apache Kafka instance + </Text> + } + aria-live="polite" + isInline + /> + </FormAlert> + )} + <FormGroup + label={"Bootstrap Server"} + labelIcon={ + <Popover bodyContent={"The bootstrap server associated with your Streams for Apache Kafka instance."}> + <button + type="button" + aria-label="More info for bootstrap server field" + onClick={(e) => e.preventDefault()} + aria-describedby="bootstrap-server-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="bootstrap-server-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="bootstrap-server-field" + name="bootstrap-server-field" + aria-label="Bootstrap server field" + aria-describedby="bootstrap-server-field-helper" + value={config.bootstrapServer} + onChange={onBootstrapServerChanged} + tabIndex={1} + data-testid="bootstrap-server-text-field" + /> + <InputGroupText> + <Button + isSmall + variant="plain" + aria-label="Clear bootstrap server button" + onClick={onClearBootstraServer} + > + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={"Topic"} + labelIcon={ + <Popover bodyContent={"The topic that messages will flow in."}> + <button + type="button" + aria-label="More info for topic field" + onClick={(e) => e.preventDefault()} + aria-describedby="topic-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="topic-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="topic-field" + name="topic-field" + aria-label="Topic field" + aria-describedby="topic-field-helper" + value={config.topic} + onChange={onTopicChanged} + tabIndex={2} + data-testid="topic-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear topic button" onClick={onClearTopic}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <TextContent> + <Text component={TextVariants.p}> + <b>Note</b>: You must also provide{" "} + <Link to={routes.settings.service_account.path({})}>Service Account</Link> so the connection with your + Streams for Apache Kafka instance can be properly established. + </Text> + </TextContent> + <ActionGroup> + <Button + isDisabled={!isCurrentConfigValid} + id="apache-kafka-config-apply-button" + key="save" + variant="primary" + onClick={onApply} + data-testid="apply-config-button" + > + Apply + </Button> + </ActionGroup> + </Form> + </Modal> + </Page> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/kafka/KafkaSettingsConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/kafka/KafkaSettingsConfig.tsx new file mode 100644 index 0000000000..dde86edea1 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/kafka/KafkaSettingsConfig.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates. + * + * 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 { makeCookieName, getCookie, setCookie } from "../../cookies"; + +export const KAFKA_BOOTSTRAP_SERVER_COOKIE_NAME = makeCookieName("kafka", "bootstrap-server"); +export const KAFKA_TOPIC_COOKIE_NAME = makeCookieName("kafka", "topic"); + +export interface KafkaSettingsConfig { + bootstrapServer: string; + topic: string; +} + +export const EMPTY_CONFIG: KafkaSettingsConfig = { + bootstrapServer: "", + topic: "", +}; + +export function isKafkaConfigValid(config: KafkaSettingsConfig): boolean { + return isBootstrapServerValid(config.bootstrapServer) && isTopicValid(config.topic); +} + +export function isBootstrapServerValid(bootstrapServer: string): boolean { + return bootstrapServer !== undefined && bootstrapServer.trim().length > 0; +} + +export function isOAuthEndpointUriValid(oauthEndpointUri: string): boolean { + return oauthEndpointUri !== undefined && oauthEndpointUri.trim().length > 0; +} + +export function isTopicValid(topic: string): boolean { + return topic !== undefined && topic.trim().length > 0; +} + +export function readKafkaConfigCookie(): KafkaSettingsConfig { + return { + bootstrapServer: getCookie(KAFKA_BOOTSTRAP_SERVER_COOKIE_NAME) ?? "", + topic: getCookie(KAFKA_TOPIC_COOKIE_NAME) ?? "", + }; +} + +export function resetConfigCookie(): void { + saveConfigCookie(EMPTY_CONFIG); +} + +export function saveBootstrapServerCookie(bootstrapServer: string): void { + setCookie(KAFKA_BOOTSTRAP_SERVER_COOKIE_NAME, bootstrapServer); +} + +export function saveTopicCookie(topic: string): void { + setCookie(KAFKA_TOPIC_COOKIE_NAME, topic); +} + +export function saveConfigCookie(config: KafkaSettingsConfig): void { + saveBootstrapServerCookie(config.bootstrapServer); + saveTopicCookie(config.topic); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettings.tsx new file mode 100644 index 0000000000..8454b667cd --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettings.tsx @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { OpenShiftConnection } from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; +import { Modal, ModalVariant } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import * as React from "react"; +import { useCallback, useState } from "react"; +import { Link } from "react-router-dom"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; +import { obfuscate } from "../github/GitHubSettings"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { saveConfigCookie } from "./OpenShiftSettingsConfig"; +import { OpenShiftSettingsSimpleConfig } from "./OpenShiftSettingsSimpleConfig"; + +export function OpenShiftSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [isModalOpen, setIsModalOpen] = useState(false); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + + const handleModalToggle = useCallback(() => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + const onDisconnect = useCallback(() => { + settingsDispatch.openshift.setStatus(OpenShiftInstanceStatus.DISCONNECTED); + const newConfig: OpenShiftConnection = { + namespace: settings.openshift.config.namespace, + host: settings.openshift.config.host, + token: "", + }; + settingsDispatch.openshift.setConfig(newConfig); + saveConfigCookie(newConfig); + }, [settings.openshift.config, settingsDispatch.openshift]); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>OpenShift</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for deploying models you design to your OpenShift instance. + <br /> + All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection> + {kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.RUNNING && ( + <> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link>{" "} + before configuring your OpenShift instance + </Text> + } + aria-live="polite" + isInline + > + KIE Sandbox Extended Services is necessary for proxying Serverless Logic Web Tools requests to OpenShift, + thus making it possible to deploy models. + </Alert> + <br /> + </> + )} + <PageSection variant={"light"}> + {settings.openshift.status === OpenShiftInstanceStatus.CONNECTED ? ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>{"You're connected to OpenShift."}</Text> + </TextContent> + <EmptyStateBody> + Deploying models is <b>enabled</b>. + <br /> + <b>Token: </b> + <i>{obfuscate(settings.openshift.config.token)}</i> + <br /> + <b>Host: </b> + <i>{settings.openshift.config.host}</i> + <br /> + <b>Namespace (project): </b> + <i>{settings.openshift.config.namespace}</i> + <br /> + <br /> + <Button variant={ButtonVariant.tertiary} onClick={onDisconnect}> + Disconnect + </Button> + </EmptyStateBody> + </EmptyState> + ) : ( + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>You are not connected to OpenShift.</Text> + </TextContent> + <EmptyStateBody> + You currently have no OpenShift connections. <br /> + <br /> + <Button variant={ButtonVariant.primary} onClick={handleModalToggle} data-testid="add-connection-button"> + Add connection + </Button> + </EmptyStateBody> + </EmptyState> + )} + </PageSection> + </PageSection> + + <Modal + title="Add connection" + isOpen={ + isModalOpen && + kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.STOPPED && + (settings.openshift.status === OpenShiftInstanceStatus.DISCONNECTED || + settings.openshift.status === OpenShiftInstanceStatus.EXPIRED) + } + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <OpenShiftSettingsSimpleConfig /> + </Modal> + </Page> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsConfig.tsx new file mode 100644 index 0000000000..26ffc405ea --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsConfig.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates. + * + * 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 { OpenShiftConnection } from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; +import { makeCookieName, getCookie, setCookie } from "../../cookies"; + +export const OPENSHIFT_NAMESPACE_COOKIE_NAME = makeCookieName("openshift", "namespace"); +export const OPENSHIFT_HOST_COOKIE_NAME = makeCookieName("openshift", "host"); +export const OPENSHIFT_TOKEN_COOKIE_NAME = makeCookieName("openshift", "token"); + +export const EMPTY_CONFIG: OpenShiftConnection = { + namespace: "", + host: "", + token: "", +}; + +export function readOpenShiftConfigCookie(): OpenShiftConnection { + return { + namespace: getCookie(OPENSHIFT_NAMESPACE_COOKIE_NAME) ?? "", + host: getCookie(OPENSHIFT_HOST_COOKIE_NAME) ?? "", + token: getCookie(OPENSHIFT_TOKEN_COOKIE_NAME) ?? "", + }; +} + +export function resetConfigCookie(): void { + saveConfigCookie(EMPTY_CONFIG); +} + +export function saveNamespaceCookie(namespace: string): void { + setCookie(OPENSHIFT_NAMESPACE_COOKIE_NAME, namespace); +} + +export function saveHostCookie(host: string): void { + setCookie(OPENSHIFT_HOST_COOKIE_NAME, host); +} + +export function saveTokenCookie(token: string): void { + setCookie(OPENSHIFT_TOKEN_COOKIE_NAME, token); +} + +export function saveConfigCookie(config: OpenShiftConnection): void { + saveNamespaceCookie(config.namespace); + saveHostCookie(config.host); + saveTokenCookie(config.token); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsSimpleConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsSimpleConfig.tsx new file mode 100644 index 0000000000..4069b01132 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/openshift/OpenShiftSettingsSimpleConfig.tsx @@ -0,0 +1,319 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates. + * + * 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 { + isOpenShiftConnectionValid, + OpenShiftConnection, +} from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button } from "@patternfly/react-core/dist/js/components/Button"; +import { ActionGroup, Form, FormAlert, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; +import { Text } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon"; +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { useAppI18n } from "../../i18n"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { EMPTY_CONFIG, saveConfigCookie } from "./OpenShiftSettingsConfig"; + +enum FormValiationOptions { + INITIAL = "INITIAL", + INVALID = "INVALID", + CONNECTION_ERROR = "CONNECTION_ERROR", + CONFIG_EXPIRED = "CONFIG_EXPIRED", +} + +export function OpenShiftSettingsSimpleConfig() { + const { i18n } = useAppI18n(); + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [config, setConfig] = useState(settings.openshift.config); + const [isConfigValidated, setConfigValidated] = useState(FormValiationOptions.INITIAL); + const [isConnecting, setConnecting] = useState(false); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + + useEffect(() => { + setConfig(settings.openshift.config); + setConfigValidated( + settings.openshift.status === OpenShiftInstanceStatus.EXPIRED + ? FormValiationOptions.CONFIG_EXPIRED + : FormValiationOptions.INITIAL + ); + }, [settings.openshift.config, settings.openshift.status]); + + const resetConfig = useCallback( + (config: OpenShiftConnection) => { + setConfigValidated( + settings.openshift.status === OpenShiftInstanceStatus.EXPIRED && config !== EMPTY_CONFIG + ? FormValiationOptions.CONFIG_EXPIRED + : FormValiationOptions.INITIAL + ); + setConnecting(false); + setConfig(config); + }, + [settings.openshift.status] + ); + + const onConnect = useCallback(async () => { + if (isConnecting) { + return; + } + + if (!isOpenShiftConnectionValid(config)) { + setConfigValidated(FormValiationOptions.INVALID); + return; + } + + setConnecting(true); + const isConfigOk = await settingsDispatch.openshift.service.isConnectionEstablished(config); + + setConnecting(false); + + if (!isConfigOk) { + setConfigValidated(FormValiationOptions.CONNECTION_ERROR); + return; + } + + saveConfigCookie(config); + settingsDispatch.openshift.setConfig(config); + resetConfig(config); + settingsDispatch.openshift.setStatus(OpenShiftInstanceStatus.CONNECTED); + }, [config, isConnecting, resetConfig, settingsDispatch.openshift]); + + const onClearHost = useCallback(() => setConfig({ ...config, host: "" }), [config]); + const onClearNamespace = useCallback(() => setConfig({ ...config, namespace: "" }), [config]); + const onClearToken = useCallback(() => setConfig({ ...config, token: "" }), [config]); + + const onHostChanged = useCallback( + (newValue: string) => { + setConfig({ ...config, host: newValue }); + }, + [config] + ); + + const onNamespaceChanged = useCallback( + (newValue: string) => { + setConfig({ ...config, namespace: newValue }); + }, + [config] + ); + + const onTokenChanged = useCallback( + (newValue: string) => { + setConfig({ ...config, token: newValue }); + }, + [config] + ); + + return ( + <> + <Form> + {kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.RUNNING && ( + <FormAlert> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link>{" "} + before configuring your OpenShift instance + </Text> + } + aria-live="polite" + isInline + /> + </FormAlert> + )} + {isConfigValidated === FormValiationOptions.INVALID && ( + <FormAlert> + <Alert + variant="danger" + title={i18n.openshift.configModal.validationError} + aria-live="polite" + isInline + data-testid="alert-validation-error" + /> + </FormAlert> + )} + {isConfigValidated === FormValiationOptions.CONNECTION_ERROR && ( + <FormAlert> + <Alert + variant="danger" + title={i18n.openshift.configModal.connectionError} + aria-live="polite" + isInline + data-testid="alert-connection-error" + /> + </FormAlert> + )} + {isConfigValidated === FormValiationOptions.CONFIG_EXPIRED && ( + <FormAlert> + <Alert + variant="warning" + title={i18n.openshift.configModal.configExpiredWarning} + aria-live="polite" + isInline + data-testid="alert-config-expired-warning" + /> + </FormAlert> + )} + <FormGroup + label={i18n.terms.namespace} + labelIcon={ + <Popover bodyContent={i18n.openshift.configModal.namespaceInfo}> + <button + type="button" + aria-label="More info for namespace field" + onClick={(e) => e.preventDefault()} + aria-describedby="namespace-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="namespace-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="namespace-field" + name="namespace-field" + aria-label="Namespace field" + aria-describedby="namespace-field-helper" + value={config.namespace} + onChange={onNamespaceChanged} + isDisabled={isConnecting} + tabIndex={1} + data-testid="namespace-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear namespace button" onClick={onClearNamespace}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={i18n.terms.host} + labelIcon={ + <Popover bodyContent={i18n.openshift.configModal.hostInfo}> + <button + type="button" + aria-label="More info for host field" + onClick={(e) => e.preventDefault()} + aria-describedby="host-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="host-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="host-field" + name="host-field" + aria-label="Host field" + aria-describedby="host-field-helper" + value={config.host} + onChange={onHostChanged} + isDisabled={isConnecting} + tabIndex={2} + data-testid="host-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear host button" onClick={onClearHost}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={i18n.terms.token} + labelIcon={ + <Popover bodyContent={i18n.openshift.configModal.tokenInfo}> + <button + type="button" + aria-label="More info for token field" + onClick={(e) => e.preventDefault()} + aria-describedby="token-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="token-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="token-field" + name="token-field" + aria-label="Token field" + aria-describedby="token-field-helper" + value={config.token} + onChange={onTokenChanged} + isDisabled={isConnecting} + tabIndex={3} + data-testid="token-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear token button" onClick={onClearToken}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <ActionGroup> + <Button + id="openshift-config-save-button" + key="save" + variant="primary" + onClick={onConnect} + data-testid="save-config-button" + isLoading={isConnecting} + isDisabled={kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.RUNNING} + spinnerAriaValueText={isConnecting ? "Loading" : undefined} + > + {isConnecting ? "Connecting" : "Connect"} + </Button> + </ActionGroup> + </Form> + </> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/routes/SettingsPageRoutes.tsx b/packages/serverless-logic-web-tools/src/newSettings/routes/SettingsPageRoutes.tsx new file mode 100644 index 0000000000..56f678e3b0 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/routes/SettingsPageRoutes.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 React from "react"; +import { Redirect, Switch } from "react-router"; +import { Route } from "react-router-dom"; +import { useRoutes } from "../../navigation/Hooks"; +import { GitHubSettings } from "../github/GitHubSettings"; +import { KieSandboxExtendedServicesSettings } from "../extendedServices/KieSandboxExtendedServicesSettings"; +import { FeaturePreviewSettings } from "../featurePreview/FeaturePreviewSettings"; +import { ApacheKafkaSettings } from "../kafka/ApacheKafkaSettings"; +import { OpenShiftSettings } from "../openshift/OpenShiftSettings"; +import { ServiceAccountSettings } from "../serviceAccount/ServiceAccountSettings"; +import { ServiceRegistrySettings } from "../serviceRegistry/ServiceRegistrySettings"; + +export function SettingsPageRoutes() { + const routes = useRoutes(); + return ( + <Switch> + <Route path={routes.settings.github.path({})}> + <GitHubSettings /> + </Route> + <Route path={routes.settings.kie_sandbox_extended_services.path({})}> + <KieSandboxExtendedServicesSettings /> + </Route> + <Route path={routes.settings.openshift.path({})}> + <OpenShiftSettings /> + </Route> + <Route path={routes.settings.service_account.path({})}> + <ServiceAccountSettings /> + </Route> + <Route path={routes.settings.service_registry.path({})}> + <ServiceRegistrySettings /> + </Route> + <Route path={routes.settings.kafka.path({})}> + <ApacheKafkaSettings /> + </Route> + <Route path={routes.settings.feature_preview.path({})}> + <FeaturePreviewSettings /> + </Route> + <Route> + <Redirect to={routes.settings.github.path({})} /> + </Route> + </Switch> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountConfig.tsx new file mode 100644 index 0000000000..1b9bb2d77a --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountConfig.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { makeCookieName, getCookie, setCookie } from "../../cookies"; + +export const SERVICE_ACCOUNT_CLIENT_ID_COOKIE_NAME = makeCookieName("service-account", "client-id"); +export const SERVICE_ACCOUNT_CLIENT_SECRET_COOKIE_NAME = makeCookieName("service-account", "client-secret"); + +export interface ServiceAccountSettingsConfig { + clientId: string; + clientSecret: string; +} + +export const EMPTY_CONFIG: ServiceAccountSettingsConfig = { + clientId: "", + clientSecret: "", +}; + +export function isServiceAccountConfigValid(config: ServiceAccountSettingsConfig): boolean { + return isClientIdValid(config.clientId) && isClientSecretValid(config.clientSecret); +} + +export function isClientIdValid(clientId: string): boolean { + return clientId !== undefined && clientId.trim().length > 0; +} + +export function isClientSecretValid(clientSecret: string): boolean { + return clientSecret !== undefined && clientSecret.trim().length > 0; +} + +export function readServiceAccountConfigCookie(): ServiceAccountSettingsConfig { + return { + clientId: getCookie(SERVICE_ACCOUNT_CLIENT_ID_COOKIE_NAME) ?? "", + clientSecret: getCookie(SERVICE_ACCOUNT_CLIENT_SECRET_COOKIE_NAME) ?? "", + }; +} + +export function resetConfigCookie(): void { + saveConfigCookie(EMPTY_CONFIG); +} + +export function saveClientIdCookie(clientId: string): void { + setCookie(SERVICE_ACCOUNT_CLIENT_ID_COOKIE_NAME, clientId); +} + +export function saveClientSecretCookie(clientSecret: string): void { + setCookie(SERVICE_ACCOUNT_CLIENT_SECRET_COOKIE_NAME, clientSecret); +} + +export function saveConfigCookie(config: ServiceAccountSettingsConfig): void { + saveClientIdCookie(config.clientId); + saveClientSecretCookie(config.clientSecret); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountSettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountSettings.tsx new file mode 100644 index 0000000000..5b7ebde91e --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/serviceAccount/ServiceAccountSettings.tsx @@ -0,0 +1,307 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Modal, ModalVariant } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { ActionGroup, Form, FormAlert, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon"; +import * as React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { EMPTY_CONFIG, isServiceAccountConfigValid, resetConfigCookie, saveConfigCookie } from "./ServiceAccountConfig"; + +export function ServiceAccountSettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [config, setConfig] = useState(settings.serviceAccount.config); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalToggle = useCallback(() => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + const isExtendedServicesRunning = useMemo( + () => kieSandboxExtendedServices.status === KieSandboxExtendedServicesStatus.RUNNING, + [kieSandboxExtendedServices.status] + ); + + const isStoredConfigValid = useMemo( + () => isExtendedServicesRunning && isServiceAccountConfigValid(settings.serviceAccount.config), + [isExtendedServicesRunning, settings.serviceAccount.config] + ); + + const isCurrentConfigValid = useMemo( + () => isExtendedServicesRunning && isServiceAccountConfigValid(config), + [config, isExtendedServicesRunning] + ); + + const onClearClientId = useCallback(() => setConfig({ ...config, clientId: "" }), [config]); + const onClearClientSecret = useCallback(() => setConfig({ ...config, clientSecret: "" }), [config]); + + const onClientIdChanged = useCallback( + (newValue: string) => { + setConfig({ ...config, clientId: newValue }); + }, + [config] + ); + + const onClientSecretChanged = useCallback( + (newValue: string) => { + setConfig({ ...config, clientSecret: newValue }); + }, + [config] + ); + + const onReset = useCallback(() => { + setConfig(EMPTY_CONFIG); + settingsDispatch.serviceAccount.setConfig(EMPTY_CONFIG); + resetConfigCookie(); + }, [settingsDispatch.serviceAccount]); + + const onApply = useCallback(() => { + settingsDispatch.serviceAccount.setConfig(config); + saveConfigCookie(config); + }, [config, settingsDispatch.serviceAccount]); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>Service Account</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for uploading Open API specs associated with models you design to your + Service Registry instance and also connecting deployments with your Streams for Apache Kafka instance. + <br /> All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection> + {kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.RUNNING && ( + <> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link>{" "} + before configuring your Service Account instance + </Text> + } + aria-live="polite" + isInline + > + KIE Sandbox Extended Services is necessary for uploading Open API specs associated with models you design + to your Service Registry instance and also connecting deployments with your Streams for Apache Kafka + instance. + </Alert> + <br /> + </> + )} + <PageSection variant={"light"}> + {isStoredConfigValid ? ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>{"Your Service Account information is set."}</Text> + </TextContent> + <EmptyStateBody> + Accessing your Service Registry and Streams for Apache Kafka is <b>enabled</b>. + <br /> + <b>Client ID: </b> + <i>{config.clientId}</i> + <br /> + <b>Client secret: </b> + <i>{obfuscate(config.clientSecret)}</i> + <br /> + <br /> + <Button variant={ButtonVariant.tertiary} onClick={onReset}> + Reset + </Button> + </EmptyStateBody> + </EmptyState> + ) : ( + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>No Service Accounts yet</Text> + </TextContent> + <EmptyStateBody> + To get started, add a service account. + <br /> + <br /> + <Button variant={ButtonVariant.primary} onClick={handleModalToggle} data-testid="add-connection-button"> + Add service account + </Button> + </EmptyStateBody> + </EmptyState> + )} + </PageSection> + </PageSection> + + <Modal + title="Add Service Account" + isOpen={ + isModalOpen && + kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.STOPPED && + !isStoredConfigValid + } + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <Form> + {!isExtendedServicesRunning && ( + <FormAlert> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}> + KIE Sandbox Extended Services + </Link>{" "} + before configuring your Service Account + </Text> + } + aria-live="polite" + isInline + /> + </FormAlert> + )} + <FormGroup + label={"Client ID"} + labelIcon={ + <Popover bodyContent={"Client ID"}> + <button + type="button" + aria-label="More info for client id field" + onClick={(e) => e.preventDefault()} + aria-describedby="client-id-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="client-id-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="client-id-field" + name="client-id-field" + aria-label="Client ID field" + aria-describedby="client-id-field-helper" + value={config.clientId} + onChange={onClientIdChanged} + tabIndex={1} + data-testid="client-id-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear client id button" onClick={onClearClientId}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={"Client Secret"} + labelIcon={ + <Popover bodyContent={"Client Secret"}> + <button + type="button" + aria-label="More info for client secret field" + onClick={(e) => e.preventDefault()} + aria-describedby="client-secret-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="client-secret-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="client-secret-field" + name="client-secret-field" + aria-label="Client secret field" + aria-describedby="client-secret-field-helper" + value={config.clientSecret} + onChange={onClientSecretChanged} + tabIndex={2} + data-testid="client-secret-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear client secret button" onClick={onClearClientSecret}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <ActionGroup> + <Button + isDisabled={!isCurrentConfigValid} + id="service-account-config-apply-button" + key="save" + variant="primary" + onClick={onApply} + data-testid="apply-config-button" + > + Apply + </Button> + </ActionGroup> + </Form> + </Modal> + </Page> + ); +} + +export function obfuscate(token?: string) { + if (!token) { + return undefined; + } + + if (token.length <= 10) { + return new Array(10).join("*"); + } + + const stars = new Array(token.length - 4).join("*"); + const pieceToObfuscate = token.substring(0, token.length - 4); + return token.replace(pieceToObfuscate, stars); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistryConfig.tsx b/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistryConfig.tsx new file mode 100644 index 0000000000..49dad4fe02 --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistryConfig.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { getCookie, makeCookieName, setCookie } from "../../cookies"; + +export const SERVICE_REGISTRY_NAME_COOKIE_NAME = makeCookieName("service-registry", "name"); +export const SERVICE_REGISTRY_CORE_REGISTRY_API_COOKIE_NAME = makeCookieName("service-registry", "core-registry-api"); + +export interface ServiceRegistrySettingsConfig { + name: string; + coreRegistryApi: string; +} + +export const EMPTY_CONFIG: ServiceRegistrySettingsConfig = { + name: "", + coreRegistryApi: "", +}; + +export function isServiceRegistryConfigValid(config: ServiceRegistrySettingsConfig): boolean { + return isNameValid(config.name) && isCoreRegistryApiValid(config.coreRegistryApi); +} + +export function isNameValid(name: string): boolean { + return name !== undefined && name.trim().length > 0; +} + +export function isCoreRegistryApiValid(coreRegistryApi: string): boolean { + return coreRegistryApi !== undefined && coreRegistryApi.trim().length > 0; +} + +export function readServiceRegistryConfigCookie(): ServiceRegistrySettingsConfig { + return { + name: getCookie(SERVICE_REGISTRY_NAME_COOKIE_NAME) ?? "", + coreRegistryApi: getCookie(SERVICE_REGISTRY_CORE_REGISTRY_API_COOKIE_NAME) ?? "", + }; +} + +export function resetConfigCookie(): void { + saveConfigCookie(EMPTY_CONFIG); +} + +export function saveNameCookie(name: string): void { + setCookie(SERVICE_REGISTRY_NAME_COOKIE_NAME, name); +} + +export function saveCoreRegistryApiCookie(coreRegistryApi: string): void { + setCookie(SERVICE_REGISTRY_CORE_REGISTRY_API_COOKIE_NAME, coreRegistryApi); +} + +export function saveConfigCookie(config: ServiceRegistrySettingsConfig): void { + saveNameCookie(config.name); + saveCoreRegistryApiCookie(config.coreRegistryApi); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistrySettings.tsx b/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistrySettings.tsx new file mode 100644 index 0000000000..2a4f4e5bfc --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/serviceRegistry/ServiceRegistrySettings.tsx @@ -0,0 +1,305 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Modal, ModalVariant } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core/dist/js/components/Alert"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { ActionGroup, Form, FormAlert, FormGroup } from "@patternfly/react-core/dist/js/components/Form"; +import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/js/components/InputGroup"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; +import { AddCircleOIcon } from "@patternfly/react-icons"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; +import HelpIcon from "@patternfly/react-icons/dist/js/icons/help-icon"; +import { TimesIcon } from "@patternfly/react-icons/dist/js/icons/times-icon"; +import * as React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { + EMPTY_CONFIG, + isServiceRegistryConfigValid, + resetConfigCookie, + saveConfigCookie, +} from "./ServiceRegistryConfig"; + +export function ServiceRegistrySettings() { + const settings = useSettings(); + const settingsDispatch = useSettingsDispatch(); + const [config, setConfig] = useState(settings.serviceRegistry.config); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalToggle = useCallback(() => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }, []); + + const isExtendedServicesRunning = useMemo( + () => kieSandboxExtendedServices.status === KieSandboxExtendedServicesStatus.RUNNING, + [kieSandboxExtendedServices.status] + ); + + const isStoredConfigValid = useMemo( + () => isExtendedServicesRunning && isServiceRegistryConfigValid(settings.serviceRegistry.config), + [isExtendedServicesRunning, settings.serviceRegistry.config] + ); + + const isCurrentConfigValid = useMemo( + () => isExtendedServicesRunning && isServiceRegistryConfigValid(config), + [isExtendedServicesRunning, config] + ); + + const onClearName = useCallback(() => setConfig({ ...config, name: "" }), [config]); + + const onClearCoreRegistryApi = useCallback(() => setConfig({ ...config, coreRegistryApi: "" }), [config]); + + const onNameChanged = useCallback((newValue: string) => setConfig({ ...config, name: newValue }), [config]); + + const onCoreRegistryApiChanged = useCallback( + (newValue: string) => setConfig({ ...config, coreRegistryApi: newValue }), + [config] + ); + + const onReset = useCallback(() => { + setConfig(EMPTY_CONFIG); + settingsDispatch.serviceRegistry.setConfig(EMPTY_CONFIG); + resetConfigCookie(); + }, [settingsDispatch.serviceRegistry]); + + const onApply = useCallback(() => { + settingsDispatch.serviceRegistry.setConfig(config); + saveConfigCookie(config); + }, [config, settingsDispatch.serviceRegistry]); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>Service Registry</Text> + <Text component={TextVariants.p}> + Data you provide here is necessary for uploading Open API specs associated with models you design to your + Service Registry instance. + <br /> All information is locally stored in your browser and never shared with anyone. + </Text> + </TextContent> + </PageSection> + + <PageSection> + {!isExtendedServicesRunning && ( + <> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link>{" "} + before configuring your Service Registry instance + </Text> + } + aria-live="polite" + isInline + > + KIE Sandbox Extended Services is necessary for uploading Open API specs associated with models you design + to your Service Registry instance. + </Alert> + <br /> + </> + )} + <PageSection variant={"light"}> + {isStoredConfigValid ? ( + <EmptyState> + <EmptyStateIcon icon={CheckCircleIcon} color={"var(--pf-global--success-color--100)"} /> + <TextContent> + <Text component={"h2"}>{"Your Service Registry information is set."}</Text> + </TextContent> + <EmptyStateBody> + Uploading OpenAPI specs when deploying models is <b>enabled</b>. + <br /> + <b>Service Registry Name: </b> + <i>{config.name}</i> + <br /> + <b>Core Registry Api: </b> + <i>{config.coreRegistryApi}</i> + <br /> + <br /> + <Button variant={ButtonVariant.tertiary} onClick={onReset}> + Reset + </Button> + </EmptyStateBody> + </EmptyState> + ) : ( + <EmptyState> + <EmptyStateIcon icon={AddCircleOIcon} /> + <TextContent> + <Text component={"h2"}>No Service Registry yet</Text> + </TextContent> + <EmptyStateBody> + To get started, add a service registry. + <br /> + <br /> + <Button variant={ButtonVariant.primary} onClick={handleModalToggle} data-testid="add-connection-button"> + Add service registry + </Button> + </EmptyStateBody> + </EmptyState> + )} + </PageSection> + </PageSection> + + <Modal + title="Add Service Registry" + isOpen={ + isModalOpen && + kieSandboxExtendedServices.status !== KieSandboxExtendedServicesStatus.STOPPED && + !isStoredConfigValid + } + onClose={handleModalToggle} + variant={ModalVariant.large} + > + <Form> + {!isExtendedServicesRunning && ( + <FormAlert> + <Alert + variant="danger" + title={ + <Text> + Connect to{" "} + <Link to={routes.settings.kie_sandbox_extended_services.path({})}> + KIE Sandbox Extended Services + </Link>{" "} + before configuring your Service Registry instance + </Text> + } + aria-live="polite" + isInline + /> + </FormAlert> + )} + <FormGroup + label={"Name"} + labelIcon={ + <Popover + bodyContent={"Name to identify your Service Registry instance across the Serverless Logic Web Tools."} + > + <button + type="button" + aria-label="More info for name field" + onClick={(e) => e.preventDefault()} + aria-describedby="name-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="name-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="name-field" + name="name-field" + aria-label="Name field" + aria-describedby="name-field-helper" + value={config.name} + onChange={onNameChanged} + tabIndex={1} + data-testid="name-text-field" + /> + <InputGroupText> + <Button isSmall variant="plain" aria-label="Clear name button" onClick={onClearName}> + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <FormGroup + label={"Core Registry API"} + labelIcon={ + <Popover bodyContent={"Core Registry API URL associated with your Service Registry instance."}> + <button + type="button" + aria-label="More info for core registry api field" + onClick={(e) => e.preventDefault()} + aria-describedby="core-registry-api-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="core-registry-api-field" + > + <InputGroup className="pf-u-mt-sm"> + <TextInput + autoComplete={"off"} + isRequired + type="text" + id="core-registry-api-field" + name="core-registry-api-field" + aria-label="Core Registry API field" + aria-describedby="core-registry-api-field-helper" + value={config.coreRegistryApi} + onChange={onCoreRegistryApiChanged} + tabIndex={2} + data-testid="core-registry-api-text-field" + /> + <InputGroupText> + <Button + isSmall + variant="plain" + aria-label="Clear core registry api button" + onClick={onClearCoreRegistryApi} + > + <TimesIcon /> + </Button> + </InputGroupText> + </InputGroup> + </FormGroup> + <TextContent> + <Text component={TextVariants.p}> + <b>Note</b>: You must also provide{" "} + <Link to={routes.settings.service_account.path({})}>Service Account</Link> so the connection with your + Service Registry instance can be properly established. + </Text> + </TextContent> + <ActionGroup> + <Button + isDisabled={!isCurrentConfigValid} + id="service-registry-config-apply-button" + key="save" + variant="primary" + onClick={onApply} + data-testid="apply-config-button" + > + Apply + </Button> + </ActionGroup> + </Form> + </Modal> + </Page> + ); +} diff --git a/packages/serverless-logic-web-tools/src/newSettings/uiNav/SettingsPageNav.tsx b/packages/serverless-logic-web-tools/src/newSettings/uiNav/SettingsPageNav.tsx new file mode 100644 index 0000000000..2d0ef44cdd --- /dev/null +++ b/packages/serverless-logic-web-tools/src/newSettings/uiNav/SettingsPageNav.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 { Nav, NavItem, NavList } from "@patternfly/react-core"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import { useRoutes } from "../../navigation/Hooks"; + +export function SettingsPageNav(props: { pathname: string }) { + const routes = useRoutes(); + + return ( + <> + <div className="chr-c-app-title">Settings</div> + <Nav aria-label="Global NAV" theme="dark"> + <NavList> + <NavItem itemId={0} key={`Settings-github-nav`} isActive={props.pathname === routes.settings.github.path({})}> + <Link to={routes.settings.github.path({})}>GitHub</Link> + </NavItem> + <NavItem + itemId={0} + key={`Settings-kie_sandbox_extended_services-nav`} + isActive={props.pathname === routes.settings.kie_sandbox_extended_services.path({})} + > + <Link to={routes.settings.kie_sandbox_extended_services.path({})}>KIE Sandbox Extended Services</Link> + </NavItem> + <NavItem + itemId={0} + key={`Settings-openshift-nav`} + isActive={props.pathname === routes.settings.openshift.path({})} + > + <Link to={routes.settings.openshift.path({})}>OpenShift</Link> + </NavItem> + <NavItem + itemId={0} + key={`Settings-service_account-nav`} + isActive={props.pathname === routes.settings.service_account.path({})} + > + <Link to={routes.settings.service_account.path({})}>Service Account</Link> + </NavItem> + <NavItem + itemId={0} + key={`Settings-service_registry-nav`} + isActive={props.pathname === routes.settings.service_registry.path({})} + > + <Link to={routes.settings.service_registry.path({})}>Service Registry</Link> + </NavItem> + <NavItem itemId={0} key={`Settings-kafka-nav`} isActive={props.pathname === routes.settings.kafka.path({})}> + <Link to={routes.settings.kafka.path({})}>Streams for Apache Kafka</Link> + </NavItem> + <NavItem + itemId={0} + key={`Settings-feature_preview-nav`} + isActive={props.pathname === routes.settings.feature_preview.path({})} + > + <Link to={routes.settings.feature_preview.path({})}>Feature Preview</Link> + </NavItem> + </NavList> + </Nav> + </> + ); +} diff --git a/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx b/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx index 242e71c961..2abccb1650 100644 --- a/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx +++ b/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx @@ -25,8 +25,8 @@ import { readOpenShiftConfigCookie } from "./openshift/OpenShiftSettingsConfig"; import { OpenShiftConnection } from "@kie-tools-core/openshift/dist/service/OpenShiftConnection"; import { OpenShiftInstanceStatus } from "../openshift/OpenShiftInstanceStatus"; import { OpenShiftService } from "@kie-tools-core/openshift/dist/service/OpenShiftService"; -import { useHistory } from "react-router"; -import { QueryParams } from "../navigation/Routes"; +import { useHistory, useRouteMatch } from "react-router"; +import { QueryParams, routes } from "../navigation/Routes"; import { GITHUB_AUTH_TOKEN_COOKIE_NAME } from "./github/GitHubSettingsTab"; import { KafkaSettingsConfig, readKafkaConfigCookie } from "./kafka/KafkaSettingsConfig"; import { readServiceAccountConfigCookie, ServiceAccountSettingsConfig } from "./serviceAccount/ServiceAccountConfig"; @@ -136,6 +136,7 @@ export function SettingsContextProvider(props: any) { const history = useHistory(); const [isOpen, setOpen] = useState(false); const [activeTab, setActiveTab] = useState(SettingsTabs.GITHUB); + const isRouteInSettingsSection = useRouteMatch(routes.settings.home.path({})); useEffect(() => { setOpen(queryParams.has(QueryParams.SETTINGS)); @@ -343,8 +344,13 @@ export function SettingsContextProvider(props: any) { return ( <SettingsContext.Provider value={value}> <SettingsDispatchContext.Provider value={dispatch}> - {githubAuthStatus !== AuthStatus.LOADING && <>{props.children}</>} - <Modal title="Settings" isOpen={isOpen} onClose={close} variant={ModalVariant.large}> + {(isRouteInSettingsSection || githubAuthStatus !== AuthStatus.LOADING) && <>{props.children}</>} + <Modal + title="Settings" + isOpen={!isRouteInSettingsSection && isOpen} + onClose={close} + variant={ModalVariant.large} + > <div style={{ height: "calc(100vh * 0.5)" }} className={"kie-tools--settings-modal-content"}> <SettingsModalBody /> </div> diff --git a/packages/serverless-logic-web-tools/static/resources/style.css b/packages/serverless-logic-web-tools/static/resources/style.css index f6ecff2555..b54a7c8b6e 100644 --- a/packages/serverless-logic-web-tools/static/resources/style.css +++ b/packages/serverless-logic-web-tools/static/resources/style.css @@ -652,3 +652,15 @@ section.kie-tools--settings-tab { .pf-c-masthead__main::before { display: none; } + +.chr-c-app-title { + color: #fff; + padding: var(--pf-global--spacer--md) var(--pf-global--spacer--sm) var(--pf-global--spacer--md) + var(--pf-global--spacer--lg); + width: 100%; + border-bottom: var(--pf-global--spacer--xs) solid var(--pf-global--Color--300); + font-weight: var(--pf-global--FontWeight--semi-bold); + white-space: normal; + text-align: left; + font-size: 17px; +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
