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 20b87604b1f7f72072e595144b79dd9d5f120c25 Merge: 1e1e5ebb9a 3d075e9645 Author: fantonangeli <[email protected]> AuthorDate: Thu May 25 11:55:59 2023 +0200 Merge remote-tracking branch 'upstream/main' into KOGITO-8015-feature-preview .ci/jenkins/Jenkinsfile.vscode | 4 - .github/workflows/daily_dev_publish.yml | 22 +- .github/workflows/release_build.yml | 77 ++- .github/workflows/release_dry_run.yml | 2 +- .github/workflows/release_publish.yml | 2 +- .github/workflows/staging_build.yml | 26 +- .gitignore | 3 + .../external/ExternalDataSetClientProvider.java | 5 + .../client/external/metrics/MetricsParser.java | 7 +- .../client/external/metrics/MetricsParserTest.java | 45 +- .../dataset/def/ExternalDataSetDef.java | 17 +- .../dataset/def/ExternalServiceType.java | 57 ++ .../dataset/json/ExternalDefJSONMarshaller.java | 18 +- .../kie-editors-dev-vscode-extension/package.json | 4 +- packages/kie-sandbox-distribution/README.md | 57 ++ .../kie-sandbox-distribution/docker-compose.yaml | 26 + packages/kie-sandbox-distribution/env/index.js | 93 +++ packages/kie-sandbox-distribution/package.json | 30 + .../src/resources/kubernetes/Deployment.ts | 18 + .../src/resources/openshift/Route.ts | 14 + .../env/index.js | 20 +- .../install.js} | 15 +- .../package.json | 40 ++ .../pom.xml | 164 ++++++ .../src/main/java/org/kie/kogito/AppStartup.java | 67 +++ .../org/kie/kogito/FileStructureConstants.java | 36 ++ .../java/org/kie/kogito/HotReloadResource.java | 103 ++++ .../org/kie/kogito/StaticContentCachingFilter.java | 48 ++ .../main/java/org/kie/kogito/api/FileService.java | 44 ++ .../java/org/kie/kogito/api/FileValidation.java} | 13 +- .../java/org/kie/kogito/api/UploadService.java} | 16 +- .../main/java/org/kie/kogito/api/ZipService.java} | 16 +- .../kie/kogito/health/LivenessHealthCheck.java} | 20 +- .../kie/kogito/health/ReadinessHealthCheck.java} | 20 +- .../org/kie/kogito/health/StartupHealthCheck.java} | 20 +- .../main/java/org/kie/kogito/model/FileType.java} | 11 +- .../org/kie/kogito/model/FileValidationResult.java | 71 +++ .../org/kie/kogito/model/UploadException.java} | 12 +- .../org/kie/kogito/model/ValidationException.java} | 20 +- .../org/kie/kogito/service/FileServiceImpl.java | 189 +++++++ .../org/kie/kogito/service/UploadServiceImpl.java | 135 +++++ .../org/kie/kogito/service/ZipServiceImpl.java | 60 ++ .../kie/kogito/validation/OpenApiValidation.java | 69 +++ .../kogito/validation/PropertiesValidation.java | 41 ++ .../validation/ServerlessWorkflowValidation.java | 65 +++ .../src/main/resources/application.properties | 12 + .../src/main/resources/hello.sw.json | 26 + .../org/kie/kogito/service/FileServiceTest.java | 210 +++++++ .../org/kie/kogito/service/ZipServiceTest.java | 52 ++ .../src/test/resources/test-with-invalid.zip | Bin 0 -> 1943 bytes .../src/test/resources/test.zip | Bin 0 -> 1415 bytes .../env/index.js | 43 ++ .../package.json | 18 + .../.dockerignore | 4 + .../Containerfile | 46 ++ .../Dockerfile | 1 + .../env/index.js | 41 ++ .../package.json | 34 ++ .../build/defaultEnvJson.ts} | 25 +- packages/serverless-logic-web-tools/env/index.js | 43 +- packages/serverless-logic-web-tools/package.json | 10 +- packages/serverless-logic-web-tools/src/App.tsx | 19 +- .../serverless-logic-web-tools/src/AppConstants.ts | 4 + .../src/ResponsiveDropdown/ResponsiveDropdown.tsx | 1 + .../src/accelerator/Accelerators.ts | 43 ++ .../src/accelerator/useAccelerator.tsx | 272 +++++++++ .../src/alerts/Alerts.tsx | 10 +- .../src/alerts/GlobalAlertsContext.tsx | 122 ++++ .../src/editor/CreateGitHubRepositoryModal.tsx | 170 +++--- .../src/editor/Deploy/ConfirmDeployModal.tsx | 9 +- .../src/editor/Deploy/DeployDropdownItems.tsx | 154 ----- .../src/editor/EditorPage.tsx | 16 +- .../src/editor/EditorToolbar.tsx | 68 +-- .../KieSandboxExtendedServicesButtons.tsx | 7 +- .../KieSandboxExtendedServicesDropdownGroup.tsx | 11 +- .../src/editor/NewFileDropdownMenu.tsx | 6 +- .../editor/api/RemoteServiceRegistryCatalogApi.ts | 2 +- .../src/editor/hooks/EditorContext.tsx | 49 ++ .../src/editor/hooks/useDeployDropdownItems.tsx | 411 ++++++++++++++ .../src/editor/hooks/useEditorNotifications.tsx | 8 +- .../src/env/EnvContext.tsx | 15 +- .../src/env/EnvContextProvider.tsx | 14 +- .../OpenShiftConstants.ts => env/EnvJson.ts} | 10 +- .../src/extension/index.ts | 16 +- .../src/fetch/index.ts} | 17 +- .../src/home/sample/{sampleApi.ts => SampleApi.ts} | 46 +- .../src/home/sample/SampleCard.tsx | 2 +- .../src/home/sample/SampleConstants.ts} | 23 +- .../src/home/sample/SamplesCatalog.tsx | 2 +- .../src/home/sample/hooks/SampleContext.tsx | 22 +- .../src/homepage/recentModels/RecentModels.tsx | 10 +- .../recentModels/workspaceFiles/WorkspaceFiles.tsx | 11 +- .../serverless-logic-web-tools/src/i18n/AppI18n.ts | 70 --- .../src/i18n/locales/en.ts | 78 +-- .../KieSandboxExtendedServicesContextProvider.tsx | 58 +- .../KieSandboxExtendedServicesIcon.tsx | 2 +- .../KieSandboxExtendedServicesModal.tsx | 12 +- .../src/openshift/DeployConstants.ts} | 14 +- .../src/openshift/OpenShiftConstants.ts | 4 + .../src/openshift/OpenShiftContext.tsx | 6 +- .../src/openshift/OpenShiftContextProvider.tsx | 55 +- .../src/openshift/deploy/BaseContainerImages.ts | 11 +- .../src/openshift/deploy/DeploymentStrategy.ts | 17 - .../strategies/DashboardSingleModelDeployment.ts | 3 +- .../strategies/DashboardWorkspaceDeployment.ts | 3 +- .../deploy/strategies/KogitoProjectDeployment.ts | 3 +- .../deploy/strategies/KogitoSwfModelDeployment.ts | 3 +- .../src/openshift/deploy/types.ts | 14 +- .../dropdown/OpenShiftDeploymentDropdownItem.tsx | 77 ++- .../dropdown/OpenshiftDeploymentsDropdown.tsx | 188 +++++-- .../src/openshift/hooks/useDeploymentStrategy.ts | 3 +- .../pipelines/DevModeDeploymentLoaderPipeline.ts | 123 ++++ .../pipelines/KnativeDeploymentLoaderPipeline.ts | 1 + .../openshift/pipelines/RestartDevModePipeline.ts | 112 ++++ .../openshift/pipelines/SpinUpDevModePipeline.ts | 241 ++++++++ .../src/openshift/swfDevMode/DevModeConstants.ts | 83 +++ .../src/openshift/swfDevMode/DevModeContext.tsx | 292 ++++++++++ .../src/settings/SettingsContext.tsx | 29 +- .../src/settings/openshift/OpenShiftSettings.tsx | 9 +- .../settings/openshift/OpenShiftSettingsConfig.tsx | 10 + .../openshift/OpenShiftSettingsSimpleConfig.tsx | 50 +- .../src/settings/storage/StorageSettings.tsx | 10 +- .../src/upgrade/UpgradeContext.tsx | 67 +++ .../components/NewWorkspaceFromSample.tsx | 4 +- .../hooks/WebToolsWorkspaceContextProvider.tsx | 30 + .../src/workspace/worker/sharedWorker.ts | 6 +- .../src/zip/index.ts} | 21 +- .../serverless-logic-web-tools/static/env.json | 2 +- .../static/resources/style.css | 100 +--- packages/serverless-logic-web-tools/tsconfig.json | 11 +- .../serverless-logic-web-tools/webpack.config.js | 302 ---------- .../serverless-logic-web-tools/webpack.config.ts | 328 +++++++++++ .../package.json | 1 + .../ServerlessWorkflowCombinedEditorChannelApi.ts | 6 + ...ServerlessWorkflowCombinedEditorEnvelopeApi.ts} | 13 +- .../editor/ServerlessWorkflowCombinedEditor.tsx | 28 + .../ServerlessWorkflowCombinedEditorView.tsx | 14 +- .../src/editor/helpers/ColorNodes.ts | 45 ++ ...verlessWorkflowCombinedEditorEnvelopeApiImpl.ts | 52 ++ .../src/impl/SwfCombinedEditorChannelApiImpl.ts | 4 + .../src/impl/index.ts | 1 + .../ServerlessWorkflowDiagramEditorEnvelopeApi.ts | 4 +- .../src/api/StunnerAPI.ts | 76 +++ .../src/api/StunnerEditorEnvelopeAPI.ts | 107 ++++ .../src/api/StunnerEditorEnvelopeAPIFactory.ts | 48 ++ .../src/api/SwfStunnerEditorAPI.ts | 182 ++++++ .../envelope/ServerlessWorkflowDiagramEditor.ts | 93 ++- ...rverlessWorkflowDiagramEditorEnvelopeApiImpl.ts | 110 +++- .../envelope/ServerlessWorkflowStunnerEditor.ts | 108 ++++ .../canvas/controls/ControlPointControlImpl.java | 2 +- .../client/widgets/editor/StunnerEditor.java | 1 + .../common/stunner/core/graph/content/Bound.java | 7 +- .../common/stunner/core/graph/content/Bounds.java | 7 +- .../core/graph/content/view/ControlPoint.java | 9 +- .../stunner/core/graph/content/view/Point2D.java | 9 +- .../stunner/core/client/api/JsStunnerEditor.java | 2 + .../stunner/core/client/api/JsStunnerSession.java | 21 +- .../common/stunner/core/client/api/JsWindow.java | 1 + .../core/graph/content/view/MagnetConnection.java | 9 +- .../core/graph/content/view/Point2DConnection.java | 7 +- .../core/graph/content/view/ViewConnectorImpl.java | 9 +- .../stunner/core/graph/content/view/ViewImpl.java | 7 +- .../common/stunner/core/graph/impl/NodeImpl.java | 8 +- .../impl/AbstractControlPointCommandTest.java | 6 +- .../com/ait/lienzo/client/core/types/JsCanvas.java | 5 + .../stunner/sw/client/editor/DiagramEditor.java | 2 +- .../common/stunner/sw/KogitoSWEditor.gwt.xml | 2 +- .../sw/client/editor/DiagramEditorTest.java | 6 +- ...neServerlessWorkflowCombinedEditorChannelApi.ts | 4 + .../src/main/resources}/hello-world.sw.json | 0 ...-workflow-editor-extension-svg-filepath.test.ts | 123 ++++ .../package.json | 4 +- packages/stunner-editors/pom.xml | 4 +- .../package.json | 4 +- .../src/VSCodeTestHelper.ts | 40 ++ .../package.json | 2 +- .../.mocharc.json | 7 - .../.vscode/launch.json | 24 - .../.vscode/settings.json | 9 - .../.vscode/tasks.json | 18 - .../CHANGELOG.md | 149 ----- .../LICENSE | 201 ------- .../README.md | 86 --- .../icon.png | Bin 22915 -> 0 bytes .../it-tests/helpers/swf/SwfEditorTestHelper.ts | 174 ------ .../autocompletion/autocompletion.sw.json | 15 - .../autocompletion/autocompletion.sw.json.result | 28 - .../autocompletion/autocompletion.sw.yaml | 16 - .../autocompletion/autocompletion.sw.yaml.result | 27 - .../emptyfile_autocompletion.sw.json | 0 .../emptyfile_autocompletion.sw.json.result | 41 -- .../emptyfile_autocompletion.sw.yaml | 0 .../emptyfile_autocompletion.sw.yaml.result | 25 - .../emptyworkflow_autocompletion.sw.json | 0 .../emptyworkflow_autocompletion.sw.json.result | 11 - .../resources/autocompletion/specs/api.yaml | 15 - .../resources/basic-operations/greet.sw.json | 67 --- .../applicant-request-decision.sw.json | 59 -- .../resources/expression/expression.sw.json | 35 -- .../resources/expression/expression.sw.yaml | 22 - .../resources/expression/schema/schema.json | 24 - .../resources/expression/specs/openapi.json | 49 -- .../it-tests/resources/functions/function.sw.json | 14 - .../resources/functions/function.sw.json.result | 30 - .../it-tests/resources/functions/function.sw.yaml | 10 - .../resources/functions/function.sw.yaml.result | 18 - .../it-tests/resources/functions/routes/camel.json | 29 - .../it-tests/resources/functions/routes/camel.yaml | 12 - .../resources/functions/specs/asyncapi.json | 17 - .../resources/functions/specs/asyncapi.yaml | 10 - .../resources/functions/specs/openapi.json | 41 -- .../resources/functions/specs/openapi.yaml | 25 - .../it-tests/resources/greeting-flow/.dockerignore | 5 - .../it-tests/resources/greeting-flow/.gitignore | 39 -- .../it-tests/resources/greeting-flow/pom.xml | 144 ----- .../org/kie/tools/it/tests/GreetingResource.java | 16 - .../src/main/resources/application.properties | 1 - .../src/main/resources/greetings.sw.json | 81 --- .../greeting-flow/src/main/resources/openapi.yaml | 37 -- .../org/kie/tools/it/tests/GreetingResourceIT.java | 9 - .../kie/tools/it/tests/GreetingResourceTest.java | 21 - .../syntax-highlight-hello-world.sw.json | 18 - ...orkflow-editor-extension-autocompletion.test.ts | 255 --------- ...kflow-editor-extension-basic-operations.test.ts | 141 ----- ...low-editor-extension-diagram-navigation.test.ts | 84 --- ...ss-workflow-editor-extension-expression.test.ts | 85 --- ...ess-workflow-editor-extension-functions.test.ts | 177 ------ ...verless-workflow-editor-extension-smoke.test.ts | 62 -- ...kflow-editor-extension-syntax-highlight.test.ts | 96 ---- .../it-tests/settings.json | 14 - .../jsonLanguageConfiguration.json | 23 - .../yamlLanguageConfiguration.json | 38 -- .../mocha-reporter-config.json | 15 - .../package.json | 388 ------------- .../src/extension/RedHatAuthExtensionStateStore.ts | 53 -- ...erverlessWorkflowDiagramEditorChannelApiImpl.ts | 261 --------- ...rlessWorkflowDiagramEditorChannelApiProducer.ts | 70 --- .../builtInVsCodeEditorSwfContributions.ts | 320 ----------- .../src/extension/commandIds.ts | 30 - .../src/extension/configuration.ts | 152 ----- .../src/extension/extension.ts | 138 ----- .../fs/JqExpressionsReadSchemaFromFs.ts | 62 -- .../SwfLanguageServiceChannelApiImpl.ts | 45 -- .../languageService/VsCodeSwfLanguageService.ts | 212 ------- .../SwfServiceCatalogChannelApiImpl.ts | 63 --- .../serviceCatalog/SwfServiceCatalogStore.ts | 71 --- .../SwfServiceCatalogSupportActions.ts | 74 --- .../fs/FsWatchingServiceCatalogRelativeStore.ts | 220 -------- .../serviceRegistry/ServiceRegistriesStore.ts | 174 ------ .../ServiceRegistryInstanceClient.ts | 88 --- .../serviceRegistry/auth/AuthProviderFactory.ts | 45 -- .../serviceRegistry/auth/RhhccAuthProvider.ts | 77 --- .../serviceCatalog/serviceRegistryCommands.ts | 50 -- .../src/extension/setupDeprecationNotification.ts | 49 -- .../extension/setupDiagramEditorCompanionTab.ts | 142 ----- .../ServerlessWorkflowDiagramEditorEnvelopeApp.ts | 44 -- .../ServerlessWorkflowMermaidViewerEnvelopeApp.ts | 28 - .../static/svg-icon-dark.png | Bin 656 -> 0 bytes .../static/svg-icon-light.png | Bin 362 -> 0 bytes .../syntaxes/JSON.tmLanguage.json | 213 ------- .../syntaxes/YAML.tmLanguage.json | 621 --------------------- .../tsconfig.it-tests.json | 11 - .../tsconfig.json | 9 - .../webpack.config.js | 72 --- .../pom.xml | 7 +- .../internal/handlers/GetAccessorsHandler.java | 12 +- .../src/textEditor/YardTextEditorController.ts | 2 + .../augmentation/language/schemas/yardSchema.ts | 187 +++++++ .../augmentation/language/yaml/index.ts} | 37 +- packages/yard-vscode-extension/package.json | 13 +- pnpm-lock.yaml | 339 +++++------ repo/graph.dot | 22 +- repo/graph.json | 86 +-- scripts/build-env/src/bin.ts | 20 + scripts/sparse-checkout/run.sh | 2 +- 275 files changed, 6682 insertions(+), 8118 deletions(-) diff --cc packages/serverless-logic-web-tools/package.json index b80490fa23,86b1bacfcc..1368fb841f --- a/packages/serverless-logic-web-tools/package.json +++ b/packages/serverless-logic-web-tools/package.json @@@ -72,7 -72,8 +74,9 @@@ "react-if": "^4.1.4", "react-router": "^5.2.1", "react-router-dom": "^5.2.1", + "short-unique-id": "^4.4.4", + "showdown": "^2.1.0", + "uuid": "^8.3.2", "vscode-languageserver-types": "^3.16.0", "yaml": "^2.0.1" }, diff --cc packages/serverless-logic-web-tools/src/AppConstants.ts index 8124e259f5,0892f4e600..4b6f3ad5ac --- a/packages/serverless-logic-web-tools/src/AppConstants.ts +++ b/packages/serverless-logic-web-tools/src/AppConstants.ts @@@ -15,5 -15,8 +15,9 @@@ */ export const APP_NAME = "Serverless Logic Web Tools"; - +export const SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL = + "https://kiegroup.github.io/kogito-docs/serverlessworkflow/latest/tooling/serverless-logic-web-tools/serverless-logic-web-tools-overview.html"; + export const APP_GIT_USER = { + name: APP_NAME, + email: "", + }; diff --cc packages/serverless-logic-web-tools/src/editor/EditorPage.tsx index 4fe16df52c,a0ed0c2ff9..12821815cc --- a/packages/serverless-logic-web-tools/src/editor/EditorPage.tsx +++ b/packages/serverless-logic-web-tools/src/editor/EditorPage.tsx @@@ -42,7 -42,7 +41,8 @@@ import { EditorToolbar } from "./Editor import { APP_NAME } from "../AppConstants"; import { WebToolsEmbeddedEditor, WebToolsEmbeddedEditorRef } from "./WebToolsEmbeddedEditor"; import { useEditorNotifications } from "./hooks/useEditorNotifications"; + import { useGlobalAlertsDispatchContext } from "../alerts/GlobalAlertsContext"; +import { setPageTitle } from "../PageTitle"; export interface Props { workspaceId: string; @@@ -218,50 -218,46 +218,44 @@@ export function EditorPage(props: Props ); return ( - <OnlineEditorPage> - <PromiseStateWrapper - promise={workspaceFilePromise} - pending={<LoadingSpinner />} - rejected={(errors) => <EditorPageErrorPage errors={errors} path={props.fileRelativePath} />} - resolved={(file) => ( - <> - <Page> - <EditorToolbar workspaceFile={file.workspaceFile} editor={webToolsEditor?.editor} /> - <Divider /> - <EditorPageDockDrawer - ref={editorPageDockRef} - isEditorReady={isEditorReady} - workspaceFile={file.workspaceFile} - onNotificationClick={onNotificationClick} - isDisabled={!webToolsEditor?.notificationHandler.isSupported} - > - <PageSection hasOverflowScroll={true} padding={{ default: "noPadding" }} aria-label="Editor Section"> - <div style={{ height: "100%" }}> - {!isEditorReady && <LoadingSpinner />} - <div style={{ display: isEditorReady ? "inline" : "none" }}> - {embeddedEditorFile && ( - <WebToolsEmbeddedEditor - uniqueFileId={uniqueFileId} - ref={webToolsEditorRef} - file={embeddedEditorFile} - workspaceFile={file.workspaceFile} - editorEnvelopeLocator={editorEnvelopeLocator} - channelType={ChannelType.ONLINE_MULTI_FILE} - locale={locale} - /> - )} - </div> + <PromiseStateWrapper + promise={workspaceFilePromise} + pending={<LoadingSpinner />} + rejected={(errors) => <EditorPageErrorPage errors={errors} path={props.fileRelativePath} />} + resolved={(file) => ( + <> + <Page> - <EditorToolbar - workspaceFile={file.workspaceFile} - editor={webToolsEditor?.editor} - alerts={alerts} - alertsRef={alertsRef} - editorPageDock={editorPageDock} - /> ++ <EditorToolbar workspaceFile={file.workspaceFile} editor={webToolsEditor?.editor} /> + <Divider /> + <EditorPageDockDrawer + ref={editorPageDockRef} + isEditorReady={isEditorReady} + workspaceFile={file.workspaceFile} + onNotificationClick={onNotificationClick} + isDisabled={!webToolsEditor?.notificationHandler.isSupported} + > + <PageSection hasOverflowScroll={true} padding={{ default: "noPadding" }} aria-label="Editor Section"> + <div style={{ height: "100%" }}> + {!isEditorReady && <LoadingSpinner />} + <div style={{ display: isEditorReady ? "inline" : "none" }}> + {embeddedEditorFile && ( + <WebToolsEmbeddedEditor + uniqueFileId={uniqueFileId} + ref={webToolsEditorRef} + file={embeddedEditorFile} + workspaceFile={file.workspaceFile} + editorEnvelopeLocator={editorEnvelopeLocator} + channelType={ChannelType.ONLINE_MULTI_FILE} + locale={locale} + /> + )} </div> - </PageSection> - </EditorPageDockDrawer> - </Page> - </> - )} - /> - </OnlineEditorPage> + </div> + </PageSection> + </EditorPageDockDrawer> + </Page> + </> + )} + /> ); } diff --cc packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx index 50a4dc4635,f385ac1bd6..46ae59ab4b --- a/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx +++ b/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx @@@ -93,15 -92,12 +91,14 @@@ import { WorkspaceStatusIndicator } fro import { WorkspaceKind } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceOrigin"; import { useEditorEnvelopeLocator } from "../envelopeLocator/EditorEnvelopeLocatorContext"; import { UrlType, useImportableUrl } from "../workspace/hooks/ImportableUrlHooks"; + import { useEnv } from "../env/EnvContext"; + import { useGlobalAlert, useGlobalAlertsDispatchContext } from "../alerts/GlobalAlertsContext"; +import { Link } from "react-router-dom"; +import { routes } from "../navigation/Routes"; export interface Props { - alerts: AlertsController | undefined; - alertsRef: (controller: AlertsController) => void; editor: EmbeddedEditorRef | undefined; workspaceFile: WorkspaceFile; - editorPageDock: EditorPageDockDrawerRef | undefined; } const showWhenSmall: ToolbarItemProps["visibility"] = { diff --cc packages/serverless-logic-web-tools/src/editor/NewFileDropdownMenu.tsx index 5996576438,fb59b77f64..62782c4830 --- a/packages/serverless-logic-web-tools/src/editor/NewFileDropdownMenu.tsx +++ b/packages/serverless-logic-web-tools/src/editor/NewFileDropdownMenu.tsx @@@ -38,7 -38,7 +37,8 @@@ import { FileTypes } from "@kie-tools-c import { decoder } from "@kie-tools-core/workspaces-git-fs/dist/encoderdecoder/EncoderDecoder"; import { extractExtension } from "@kie-tools-core/workspaces-git-fs/dist/relativePath/WorkspaceFileRelativePathParser"; import { UrlType } from "../workspace/hooks/ImportableUrlHooks"; + import { useGlobalAlert } from "../alerts/GlobalAlertsContext"; +import { ValidatedOptions } from "@patternfly/react-core/dist/js"; const ROOT_MENU_ID = "addFileRootMenu"; diff --cc packages/serverless-logic-web-tools/src/editor/hooks/useDeployDropdownItems.tsx index 0000000000,05183e17d4..e0f1e6be5b mode 000000,100644..100644 --- a/packages/serverless-logic-web-tools/src/editor/hooks/useDeployDropdownItems.tsx +++ b/packages/serverless-logic-web-tools/src/editor/hooks/useDeployDropdownItems.tsx @@@ -1,0 -1,410 +1,411 @@@ + /* + * 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 { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; + import { Divider } from "@patternfly/react-core/dist/js/components/Divider"; + import { Spinner } from "@patternfly/react-core/dist/js/components/Spinner"; + import { List, ListItem } from "@patternfly/react-core/dist/js/components/List"; + import { DropdownItem } from "@patternfly/react-core/dist/js/components/Dropdown"; + import { Text } from "@patternfly/react-core/dist/js/components/Text"; + import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; + import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex"; + import { RegistryIcon, ExclamationCircleIcon } from "@patternfly/react-icons/dist/js/icons"; + import { OpenshiftIcon } from "@patternfly/react-icons/dist/js/icons/openshift-icon"; + import { UploadIcon } from "@patternfly/react-icons/dist/js/icons/upload-icon"; + import * as React from "react"; + import { useCallback, useMemo, useState, useEffect } from "react"; + import { useAppI18n } from "../../i18n"; + import { FeatureDependentOnKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/FeatureDependentOnKieSandboxExtendedServices"; + import { + DependentFeature, + useKieSandboxExtendedServices, + } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; + import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; + import { useOpenShift } from "../../openshift/OpenShiftContext"; + import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; -import { useSettings, useSettingsDispatch } from "../../settings/SettingsContext"; -import { SettingsTabs } from "../../settings/SettingsModalBody"; ++import { useSettings } from "../../settings/SettingsContext"; + import { useVirtualServiceRegistryDependencies } from "../../virtualServiceRegistry/hooks/useVirtualServiceRegistryDependencies"; + import { FileLabel } from "../../workspace/components/FileLabel"; + import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace"; + import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; + import { useDevMode, useDevModeDispatch } from "../../openshift/swfDevMode/DevModeContext"; + import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/js/components/Alert"; + import { useEnv } from "../../env/EnvContext"; + import { useGlobalAlert } from "../../alerts/GlobalAlertsContext"; + import { useEditor } from "../hooks/EditorContext"; + import { isOfKind } from "@kie-tools-core/workspaces-git-fs/dist/constants/ExtensionHelper"; ++import { useHistory } from "react-router"; ++import { routes } from "../../navigation/Routes"; + + const FETCH_DEV_MODE_DEPLOYMENT_POLLING_TIME = 2000; + + interface Props { + workspace: ActiveWorkspace; + workspaceFile: WorkspaceFile; + } + + export function useDeployDropdownItems(props: Props) { + const { env } = useEnv(); + const { notifications } = useEditor(); + const { i18n } = useAppI18n(); + const devMode = useDevMode(); + const devModeDispatch = useDevModeDispatch(); + const settings = useSettings(); - const settingsDispatch = useSettingsDispatch(); + const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const openshift = useOpenShift(); + const [canContentBeDeployed, setCanContentBeDeployed] = useState(true); + const { needsDependencyDeployment } = useVirtualServiceRegistryDependencies({ + workspace: props.workspace, + }); ++ const history = useHistory(); + + useEffect(() => { + props.workspaceFile.getFileContentsAsString().then((content) => { + setCanContentBeDeployed(content.trim().length > 0 && !notifications.some((d) => d.severity === "ERROR")); + }); + }, [notifications, props.workspaceFile]); + + const devModeUploadingAlert = useGlobalAlert( + useCallback(({ close }) => { + return ( + <Alert + className="pf-u-mb-md" + variant="info" + title={ + <> + <Spinner size={"sm"} /> + Uploading files to Dev Mode... + </> + } + aria-live="polite" + data-testid="alert-dev-mode-uploading" + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const devModeReadyAlert = useGlobalAlert<{ routeUrl: string; filePaths: string[] }>( + useCallback(({ close }, { routeUrl, filePaths }) => { + return ( + <Alert + isExpandable + className="pf-u-mb-md" + variant="success" + title={"Your Dev Mode has been successfully updated"} + aria-live="polite" + data-testid="alert-dev-mode-ready" + actionClose={<AlertActionCloseButton onClose={close} />} + actionLinks={ + <AlertActionLink onClick={() => window.open(routeUrl, "_blank")}> + {"Go to Serverless Workflow Dev UI ↗"} + </AlertActionLink> + } + > + <> + <Text component="p" style={{ marginBottom: "4px" }}> + Files that have been uploaded: + </Text> + <List> + {filePaths.map((p) => ( + <ListItem key={`uploaded-file-path-${p}`}>{p}</ListItem> + ))} + </List> + </> + </Alert> + ); + }, []) + ); + + const uploadToDevModeSuccessAlert = useGlobalAlert( + useCallback(({ close }) => { + return ( + <Alert + className="pf-u-mb-md" + variant="info" + title={ + <> + <Spinner size={"sm"} /> + Updating the Dev Mode deployment... + </> + } + aria-live="polite" + data-testid="alert-dev-mode-updating" + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const uploadToDevModeErrorAlert = useGlobalAlert<{ messages: string[] }>( + useCallback(({ close }, { messages }) => { + return ( + <Alert + isExpandable + className="pf-u-mb-md" + variant="warning" + title={"Something went wrong while uploading to the Dev Mode."} + aria-live="polite" + data-testid="alert-upload-error" + actionClose={<AlertActionCloseButton onClose={close} />} + > + {messages.length > 1 ? ( + <List> + {messages.map((p) => ( + <ListItem key={`error-message-upload-${p}`}>{p}</ListItem> + ))} + </List> + ) : ( + <Text component="p">{messages[0]}</Text> + )} + </Alert> + ); + }, []) + ); + + const uploadToDevModeTimeoutErrorAlert = useGlobalAlert( + useCallback(({ close }) => { + return ( + <Alert + className="pf-u-mb-md" + variant="warning" + title={ + <> + Something went wrong while uploading to the Dev Mode. + <br /> + Please check your Dev Mode deployment logs for more details. + </> + } + aria-live="polite" + data-testid="alert-upload-error" + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const isKieSandboxExtendedServicesRunning = useMemo( + () => kieSandboxExtendedServices.status === KieSandboxExtendedServicesStatus.RUNNING, + [kieSandboxExtendedServices.status] + ); + + const isOpenShiftConnected = useMemo( + () => settings.openshift.status === OpenShiftInstanceStatus.CONNECTED, + [settings.openshift.status] + ); + + const isUploadToDevModeEnabled = useMemo( + () => devMode.isEnabled && isOfKind("sw", props.workspaceFile.name), + [devMode.isEnabled, props.workspaceFile.name] + ); + + const onSetup = useCallback(() => { - settingsDispatch.open(SettingsTabs.OPENSHIFT); - }, [settingsDispatch]); ++ history.push(routes.settings.openshift.path({})); ++ }, [history]); + + const onDeploy = useCallback(() => { + if (isKieSandboxExtendedServicesRunning) { + openshift.setConfirmDeployModalOpen(true); + return; + } + kieSandboxExtendedServices.setInstallTriggeredBy(DependentFeature.OPENSHIFT); + kieSandboxExtendedServices.setModalOpen(true); + }, [isKieSandboxExtendedServicesRunning, kieSandboxExtendedServices, openshift]); + + const onUploadDevMode = useCallback(async () => { + if (isKieSandboxExtendedServicesRunning) { + devModeUploadingAlert.show(); + const result = await devModeDispatch.upload({ + targetSwfFile: props.workspaceFile, + allFiles: props.workspace.files, + }); + devModeUploadingAlert.close(); + + if (result.success) { + uploadToDevModeSuccessAlert.show(); + + let attemptsLeft = 15; + const fetchDevModeDeploymentTask = window.setInterval(async () => { + const isReady = await devModeDispatch.checkHealthReady(); + attemptsLeft--; + if (attemptsLeft === 0) { + uploadToDevModeSuccessAlert.close(); + uploadToDevModeTimeoutErrorAlert.show(); + window.clearInterval(fetchDevModeDeploymentTask); + return; + } + if (!isReady) { + return; + } + uploadToDevModeSuccessAlert.close(); + devModeReadyAlert.show({ routeUrl: devMode.endpoints!.swfDevUi, filePaths: result.uploadedPaths }); + window.clearInterval(fetchDevModeDeploymentTask); + }, FETCH_DEV_MODE_DEPLOYMENT_POLLING_TIME); + } else { + uploadToDevModeErrorAlert.show({ messages: result.messages }); + } + } else { + kieSandboxExtendedServices.setInstallTriggeredBy(DependentFeature.OPENSHIFT); + kieSandboxExtendedServices.setModalOpen(true); + } + }, [ + isKieSandboxExtendedServicesRunning, + devModeUploadingAlert, + devModeDispatch, + props.workspaceFile, + props.workspace.files, + uploadToDevModeSuccessAlert, + devModeReadyAlert, + devMode.endpoints, + uploadToDevModeErrorAlert, + kieSandboxExtendedServices, + uploadToDevModeTimeoutErrorAlert, + ]); + + return useMemo(() => { + return [ + <React.Fragment key={"deploy-dropdown-items"}> + {props.workspace && ( + <FeatureDependentOnKieSandboxExtendedServices isLight={false} position="left"> + {isUploadToDevModeEnabled && ( + <DropdownItem + icon={<UploadIcon />} + id="upload-dev-mode-button" + key={`dropdown-upload-dev-mode`} + component={"button"} + onClick={onUploadDevMode} + isDisabled={isKieSandboxExtendedServicesRunning && (!isOpenShiftConnected || !canContentBeDeployed)} + ouiaId={"upload-to-openshift-dev-mode-dropdown-button"} + > + {props.workspace.files.length > 1 && ( + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + Upload <b>{`"${props.workspace.descriptor.name}"`}</b> to Dev Mode + </FlexItem> + </Flex> + )} + {props.workspace.files.length === 1 && ( + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + Upload <b>{`"${props.workspace.files[0].nameWithoutExtension}"`}</b> to Dev Mode + </FlexItem> + <FlexItem> + <b> + <FileLabel extension={props.workspace.files[0].extension} /> + </b> + </FlexItem> + </Flex> + )} + </DropdownItem> + )} + <DropdownItem + icon={<OpenshiftIcon />} + id="deploy-your-model-button" + key={`dropdown-deploy`} + component={"button"} + onClick={onDeploy} + isDisabled={isKieSandboxExtendedServicesRunning && (!isOpenShiftConnected || !canContentBeDeployed)} + ouiaId={"deploy-to-openshift-dropdown-button"} + > + {props.workspace.files.length > 1 && ( + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + Deploy models in <b>{`"${props.workspace.descriptor.name}"`}</b> + </FlexItem> + </Flex> + )} + {props.workspace.files.length === 1 && ( + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + Deploy <b>{`"${props.workspace.files[0].nameWithoutExtension}"`}</b> + </FlexItem> + <FlexItem> + <b> + <FileLabel extension={props.workspace.files[0].extension} /> + </b> + </FlexItem> + </Flex> + )} + </DropdownItem> + {needsDependencyDeployment && ( + <> + <Divider /> + <Tooltip content={i18n.deployments.virtualServiceRegistry.dependencyWarningTooltip} position="bottom"> + <DropdownItem icon={<RegistryIcon color="var(--pf-global--warning-color--100)" />} isDisabled> + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + <Text component="small" style={{ color: "var(--pf-global--warning-color--200)" }}> + This model has foreign workspace dependencies + </Text> + </FlexItem> + </Flex> + </DropdownItem> + </Tooltip> + </> + )} + {!canContentBeDeployed && ( + <> + <Divider /> + <Tooltip + content={ + "Models with errors or empty ones cannot be deployed. Check the Problems tab for more information." + } + position="bottom" + > + <DropdownItem icon={<ExclamationCircleIcon color="var(--pf-global--danger-color--100)" />} isDisabled> + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + <Text component="small" style={{ color: "var(--pf-global--danger-color--300)" }}> + This model cannot be deployed + </Text> + </FlexItem> + </Flex> + </DropdownItem> + </Tooltip> + </> + )} + </FeatureDependentOnKieSandboxExtendedServices> + )} + {!isOpenShiftConnected && isKieSandboxExtendedServicesRunning && ( + <> + <Divider /> + <DropdownItem + id="deploy-setup-button" + key={`dropdown-deploy-setup`} + onClick={onSetup} + ouiaId={"setup-deploy-dropdown-button"} + > + <Button isInline={true} variant={ButtonVariant.link}> + Setup... + </Button> + </DropdownItem> + </> + )} + </React.Fragment>, + ]; + }, [ + props.workspace, + onDeploy, + isKieSandboxExtendedServicesRunning, + isOpenShiftConnected, + canContentBeDeployed, + isUploadToDevModeEnabled, + onUploadDevMode, + needsDependencyDeployment, + i18n.deployments.virtualServiceRegistry.dependencyWarningTooltip, + onSetup, + ]); + } diff --cc packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx index aa81206b59,86e53b1dad..deed941efe --- a/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx +++ b/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx @@@ -14,19 -14,19 +14,19 @@@ * limitations under the License. */ -import * as React from "react"; -import { useMemo, useRef, useEffect } from "react"; -import { Card, CardTitle, CardFooter, CardBody } from "@patternfly/react-core/dist/js/components/Card"; -import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid"; -import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; -import { useRoutes } from "../../navigation/Hooks"; -import { Link } from "react-router-dom"; -import { Text } from "@patternfly/react-core/dist/js/components/Text"; +import { Button, Modal, ModalVariant, Skeleton } from "@patternfly/react-core/dist/js"; +import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/js/components/Card"; import { Label, LabelProps } from "@patternfly/react-core/dist/js/components/Label"; -import { FolderIcon, FileIcon, MonitoringIcon } from "@patternfly/react-icons/dist/js/icons"; + import { Sample, SampleCategory } from "./SampleApi"; +import { Text } from "@patternfly/react-core/dist/js/components/Text"; import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye"; +import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid"; +import { FileIcon, FolderIcon, MonitoringIcon, SearchPlusIcon } from "@patternfly/react-icons/dist/js/icons"; +import * as React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useRoutes } from "../../navigation/Hooks"; - import { Sample, SampleCategory } from "./sampleApi"; const tagMap: Record<SampleCategory, { label: string; icon: React.ComponentClass; color: LabelProps["color"] }> = { ["serverless-workflow"]: { diff --cc packages/serverless-logic-web-tools/src/home/sample/SampleConstants.ts index e956c766d7,b9cec21a1a..2a8a289605 --- a/packages/serverless-logic-web-tools/src/home/sample/SampleConstants.ts +++ b/packages/serverless-logic-web-tools/src/home/sample/SampleConstants.ts @@@ -14,17 -14,12 +14,14 @@@ * limitations under the License. */ - import { TextEditor, WebView } from "vscode-extension-tester"; ++export const SAMPLE_COVERS_CACHE_FILE_PATH = "/covers.json"; + - /** - * Helper class to easen work with swf text editor. - * Make sure you switch to the webview's frame before creating and instance - * via contructor. - */ - export default class SwfTextEditorTestHelper { - constructor(private readonly webview: WebView) {} + export const SAMPLES_FS_MOUNT_POINT_PREFIX = "lfs_v1__samples__"; + + export const SAMPLE_DEFINITIONS_CACHE_FILE_PATH = "/definitions.json"; + + export const SAMPLE_SEARCH_KEYS = ["definition.category", "definition.title", "definition.description"]; - public async getSwfTextEditor(): Promise<TextEditor> { - return Promise.resolve(new TextEditor()); - } - } + export const resolveSampleFsMountPoint = (appVersion: string) => { + return `${SAMPLES_FS_MOUNT_POINT_PREFIX}${appVersion}`; + }; diff --cc packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx index 76258ddfb2,0000000000..619bd13d4a mode 100644,000000..100644 --- a/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx +++ b/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx @@@ -1,309 -1,0 +1,309 @@@ +/* + * 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 { Pagination, PaginationVariant, PerPageOptions, Skeleton } from "@patternfly/react-core/dist/js"; +import { + Dropdown, + DropdownItem, + DropdownSeparator, + DropdownToggle, +} from "@patternfly/react-core/dist/js/components/Dropdown"; +import { EmptyState, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { Title } from "@patternfly/react-core/dist/js/components/Title"; +import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/js/components/Toolbar"; +import { Gallery } from "@patternfly/react-core/dist/js/layouts/Gallery"; +import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; +import * as React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useHistory, useLocation } from "react-router"; +import { QueryParams } from "../../navigation/Routes"; +import { setPageTitle } from "../../PageTitle"; +import { useQueryParam } from "../../queryParams/QueryParamsContext"; +import { FileLabel } from "../../workspace/components/FileLabel"; +import { useSampleDispatch } from "./hooks/SampleContext"; - import { Sample, SampleCategories, SampleCategory, SampleCoversHashtable } from "./sampleApi"; ++import { Sample, SampleCategories, SampleCategory, SampleCoversHashtable } from "./SampleApi"; +import { SampleCard } from "./SampleCard"; +import { SampleCardSkeleton } from "./SampleCardSkeleton"; +import { SamplesLoadError } from "./SamplesLoadError"; + +type SearchParams = { searchValue: string; category?: SampleCategory }; + +const PAGE_TITLE = "Samples Catalog"; +const SAMPLE_PRIORITY: Record<SampleCategory, number> = { + ["serverless-workflow"]: 1, + ["dashbuilder"]: 2, + ["serverless-decision"]: 3, +}; + +const LABEL_MAP: Record<SampleCategory, JSX.Element> = { + ["serverless-workflow"]: <FileLabel extension="sw.yaml" labelProps={{ isCompact: true }} />, + ["dashbuilder"]: <FileLabel extension="dash.yaml" labelProps={{ isCompact: true }} />, + ["serverless-decision"]: <FileLabel extension="yard.yaml" labelProps={{ isCompact: true }} />, +}; + +const ALL_CATEGORIES_LABEL = "All categories"; +const CARDS_PER_PAGE = 9; +const CATEGORY_ARRAY = Object.keys(SAMPLE_PRIORITY) as SampleCategory[]; + +export const SAMPLE_CARDS_PER_PAGE_OPTIONS: PerPageOptions[] = [ + { + title: `${CARDS_PER_PAGE}`, + value: CARDS_PER_PAGE, + }, +]; + +export function SamplesCatalog() { + const sampleDispatch = useSampleDispatch(); + const [loading, setLoading] = useState<boolean>(true); + const [samples, setSamples] = useState<Sample[]>([]); + const [sampleCovers, setSampleCovers] = useState<SampleCoversHashtable>({}); + const [sampleLoadingError, setSampleLoadingError] = useState(""); + const [searchFilter, setSearchFilter] = useState(""); + const [searchParams, setSearchParams] = useState<SearchParams | undefined>(undefined); + const [page, setPage] = React.useState(1); + const [isCategoryFilterDropdownOpen, setCategoryFilterDropdownOpen] = useState(false); + const history = useHistory(); + const location = useLocation(); + + const categoryFilter = useQueryParam(QueryParams.SAMPLES_CATEGORY) as SampleCategory; + + const visibleSamples = useMemo( + () => samples.slice((page - 1) * CARDS_PER_PAGE, page * CARDS_PER_PAGE), + [samples, page] + ); + + const samplesCount = useMemo(() => samples.length, [samples]); + + const filterResultMessage = useMemo(() => { + if (samplesCount === 0) { + return; + } + const isPlural = samplesCount > 1; + return `Showing ${samplesCount} sample${isPlural ? "s" : ""}`; + }, [samplesCount]); + + const selectedCategory = useMemo(() => { + if (categoryFilter) { + return LABEL_MAP[categoryFilter]; + } + return ALL_CATEGORIES_LABEL; + }, [categoryFilter]); + + const setCategoryFilter = useCallback( + (category?: SampleCategory) => { + const searchParams = new URLSearchParams(location.search); + if (category) { + searchParams.set(QueryParams.SAMPLES_CATEGORY, category); + } else { + searchParams.delete(QueryParams.SAMPLES_CATEGORY); + } + const newSearchString = searchParams.toString(); + history.push({ search: newSearchString }); + }, + [history, location] + ); + + const onSearch = useCallback( + async (args: SearchParams) => { + if (searchParams && args.searchValue === searchParams.searchValue && args.category === searchParams.category) { + return; + } + setSearchFilter(args.searchValue); + setCategoryFilter(args.category); + setSearchParams(args); + setPage(1); + setSamples(await sampleDispatch.getSamples({ searchFilter: args.searchValue, categoryFilter: args.category })); + }, + [sampleDispatch, setCategoryFilter, searchParams] + ); + + useEffect(() => { + if (categoryFilter && !SampleCategories.includes(categoryFilter)) { + setCategoryFilter(undefined); + return; + } + + onSearch({ searchValue: searchFilter, category: categoryFilter }); + }, [categoryFilter, onSearch, searchFilter, setCategoryFilter]); + + useEffect(() => { + if (searchParams && searchFilter === searchParams.searchValue && categoryFilter === searchParams.category) { + return; + } + setSearchParams({ searchValue: searchFilter, category: categoryFilter }); + + sampleDispatch + .getSamples({ categoryFilter }) + .then((data) => { + const sortedSamples = data.sort( + (a: Sample, b: Sample) => SAMPLE_PRIORITY[a.definition.category] - SAMPLE_PRIORITY[b.definition.category] + ); + setSamples([...sortedSamples]); + }) + .catch((e) => { + setSampleLoadingError(e.toString()); + }) + .finally(() => { + setLoading(false); + }); + }, [sampleDispatch, categoryFilter, searchFilter, searchParams]); + + useEffect(() => { + sampleDispatch.getSampleCovers({ samples: visibleSamples, prevState: sampleCovers }).then(setSampleCovers); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visibleSamples, sampleDispatch]); + + const categoryFilterDropdownItems = useMemo( + () => [ + <DropdownItem + key="category-filter-all-categories" + onClick={() => onSearch({ searchValue: searchFilter, category: undefined })} + > + {ALL_CATEGORIES_LABEL} + </DropdownItem>, + <DropdownSeparator key="category-filter-separator" />, + ...CATEGORY_ARRAY.map((category: SampleCategory) => ( + <DropdownItem + key={`category-filter-${category}`} + onClick={() => onSearch({ searchValue: searchFilter, category })} + > + {LABEL_MAP[category]} + </DropdownItem> + )), + ], + [onSearch, searchFilter] + ); + + const onSetPage = useCallback((_e, v) => { + setPage(v); + }, []); + + useEffect(() => { + setPageTitle([PAGE_TITLE]); + }, []); + + return ( + <Page> + <PageSection variant={"light"}> + <TextContent> + <Text component={TextVariants.h1}>{PAGE_TITLE}</Text> + <Text component={TextVariants.p}>Try one of our samples to start defining your model.</Text> + </TextContent> + <Toolbar style={{ paddingBottom: "0" }}> + <ToolbarContent style={{ paddingLeft: "0", paddingRight: "0", paddingBottom: "0" }}> + <ToolbarItem variant="search-filter"> + <SearchInput + value={""} + type={"search"} + onChange={(_ev, value) => onSearch({ searchValue: value, category: categoryFilter })} + onClear={() => onSearch({ searchValue: "", category: categoryFilter })} + onFocus={() => setCategoryFilterDropdownOpen(false)} + placeholder={"Find samples"} + style={{ width: "400px" }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + </ToolbarItem> + <ToolbarItem> + <Dropdown + style={{ backgroundColor: "white" }} + onSelect={() => setCategoryFilterDropdownOpen(false)} + dropdownItems={categoryFilterDropdownItems} + toggle={ + <DropdownToggle + id="category-filter-dropdown" + onToggle={(isOpen: boolean) => setCategoryFilterDropdownOpen(isOpen)} + > + {selectedCategory} + </DropdownToggle> + } + isOpen={isCategoryFilterDropdownOpen} + /> + </ToolbarItem> + <ToolbarItem> + {filterResultMessage && ( + <TextContent> + <Text>{filterResultMessage}</Text> + </TextContent> + )} + </ToolbarItem> + <ToolbarItem variant="pagination"> + {loading && <Skeleton width="200px" />} + {!loading && ( + <Pagination + isCompact + itemCount={samplesCount} + onSetPage={onSetPage} + page={page} + perPage={CARDS_PER_PAGE} + perPageOptions={SAMPLE_CARDS_PER_PAGE_OPTIONS} + variant="top" + /> + )} + </ToolbarItem> + </ToolbarContent> + </Toolbar> + </PageSection> + + <PageSection isFilled> + {sampleLoadingError && <SamplesLoadError errors={[sampleLoadingError]} />} + {!sampleLoadingError && ( + <> + {loading && <SampleCardSkeleton numberOfCards={6} />} + {!loading && samplesCount === 0 && ( + <PageSection variant={"light"} isFilled={true} style={{ marginRight: "25px" }}> + <EmptyState style={{ height: "350px" }}> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="lg"> + {"None of the available samples matched this search"} + </Title> + </EmptyState> + </PageSection> + )} + {!loading && samplesCount > 0 && ( + <> + <Gallery hasGutter={true} minWidths={{ sm: "calc(100%/3.1 - 16px)", default: "100%" }}> + {visibleSamples.map((sample) => ( + <SampleCard + sample={sample} + key={`sample-${sample.sampleId}`} + cover={sampleCovers[sample.sampleId]} + /> + ))} + </Gallery> + <br /> + <Pagination + itemCount={samplesCount} + onSetPage={onSetPage} + page={page} + perPage={CARDS_PER_PAGE} + perPageComponent="button" + perPageOptions={SAMPLE_CARDS_PER_PAGE_OPTIONS} + variant={PaginationVariant.bottom} + widgetId="bottom-example" + /> + </> + )} + </> + )} + </PageSection> + </Page> + ); +} diff --cc packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx index 65204accd1,ad3357cf41..6a197f141f --- a/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx +++ b/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx @@@ -20,21 -20,11 +20,23 @@@ import { LocalFile } from "@kie-tools-c import * as React from "react"; import { useContext, useMemo, useCallback, useState } from "react"; import { useSettingsDispatch } from "../../../settings/SettingsContext"; -import { fetchSampleDefinitions, fetchSampleFiles, Sample, SampleCategory } from "../SampleApi"; +import { + fetchSampleCover, + fetchSampleDefinitions, + fetchSampleFiles, + Sample, + SampleCategory, + SampleCoversHashtable, - } from "../sampleApi"; ++} from "../SampleApi"; import { decoder, encoder } from "@kie-tools-core/workspaces-git-fs/dist/encoderdecoder/EncoderDecoder"; import Fuse from "fuse.js"; - - const SAMPLE_DEFINITIONS_CACHE_FILE_PATH = "/definitions.json"; - const SAMPLE_COVERS_CACHE_FILE_PATH = "/covers.json"; - const SAMPLES_FS_MOUNT_POINT = `lfs_v1__samples__${process.env.WEBPACK_REPLACE__version!}`; - const SEARCH_KEYS = ["definition.category", "definition.title", "definition.description"]; -import { SAMPLE_DEFINITIONS_CACHE_FILE_PATH, SAMPLE_SEARCH_KEYS, resolveSampleFsMountPoint } from "../SampleConstants"; ++import { ++ SAMPLE_DEFINITIONS_CACHE_FILE_PATH, ++ SAMPLE_SEARCH_KEYS, ++ resolveSampleFsMountPoint, ++ SAMPLE_COVERS_CACHE_FILE_PATH, ++} from "../SampleConstants"; + import { useEnv } from "../../../env/EnvContext"; export interface SampleDispatchContextType { getSamples(args: { categoryFilter?: SampleCategory; searchFilter?: string }): Promise<Sample[]>; diff --cc packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx index c7f3120d85,0000000000..51c3119e31 mode 100644,000000..100644 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx @@@ -1,267 -1,0 +1,263 @@@ +/* + * 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 { PromiseStateWrapper } from "@kie-tools-core/react-hooks/dist/PromiseState"; +import { useController } from "@kie-tools-core/react-hooks/dist/useController"; +import { useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; +import { useWorkspaceDescriptorsPromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspacesHooks"; +import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor"; +import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert"; +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 { Title } from "@patternfly/react-core/dist/js/components/Title"; +import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye"; +import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; +import * as React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; - import { Alerts, AlertsController, useAlert } from "../../alerts/Alerts"; ++import { useGlobalAlert } from "../../alerts/GlobalAlertsContext"; +import { splitFiles } from "../../extension"; +import { setPageTitle } from "../../PageTitle"; +import { ConfirmDeleteModal } from "../../table/ConfirmDeleteModal"; +import { defaultPerPageOptions, TablePagination } from "../../table/TablePagination"; +import { TableToolbar } from "../../table/TableToolbar"; +import { WorkspacesTable } from "./WorkspacesTable"; + +const PAGE_TITLE = "Recent models"; + +export function RecentModels() { + const workspaceDescriptorsPromise = useWorkspaceDescriptorsPromise(); + const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState<WorkspaceDescriptor["workspaceId"][]>([]); + const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); + const [searchValue, setSearchValue] = React.useState(""); + const [page, setPage] = React.useState(1); + const [perPage, setPerPage] = React.useState(5); + const workspaces = useWorkspaces(); + const [selectedFoldersCount, setSelectedFoldersCount] = useState(0); + const [firstSelectedWorkspaceName, setFirstSelectedWorkspaceName] = useState(""); + const [deleteModalDataLoaded, setDeleteModalDataLoaded] = useState(false); + const [deleteModalFetchError, setDeleteModalFetchError] = useState(false); - const [alerts, alertsRef] = useController<AlertsController>(); + const isSelectedWorkspacePlural = useMemo(() => selectedWorkspaceIds.length > 1, [selectedWorkspaceIds]); + + const selectedElementTypesName = useMemo(() => { + if (selectedWorkspaceIds.length > 1) { + return selectedFoldersCount ? "workspaces" : "models"; + } + return selectedFoldersCount ? "workspace" : "model"; + }, [selectedFoldersCount, selectedWorkspaceIds]); + + const deleteModalMessage = useMemo( + () => ( + <> + Deleting {isSelectedWorkspacePlural ? "these" : "this"}{" "} + <b>{isSelectedWorkspacePlural ? selectedWorkspaceIds.length : firstSelectedWorkspaceName}</b>{" "} + {selectedElementTypesName} + {selectedFoldersCount ? ` removes the ${selectedElementTypesName} and all the models inside.` : "."} + </> + ), + [ + isSelectedWorkspacePlural, + selectedWorkspaceIds, + firstSelectedWorkspaceName, + selectedElementTypesName, + selectedFoldersCount, + ] + ); + + const onConfirmDeleteModalClose = useCallback(() => setIsConfirmDeleteModalOpen(false), []); + - const deleteSuccessAlert = useAlert<{ modelsWord: string }>( - alerts, ++ const deleteSuccessAlert = useGlobalAlert<{ modelsWord: string }>( + useCallback(({ close }, { modelsWord }) => { + return <Alert variant="success" title={`${capitalizeString(modelsWord)} deleted successfully`} />; + }, []), + { durationInSeconds: 2 } + ); + - const deleteErrorAlert = useAlert<{ modelsWord: string }>( - alerts, ++ const deleteErrorAlert = useGlobalAlert<{ modelsWord: string }>( + useCallback(({ close }, { modelsWord }) => { + return ( + <Alert + variant="danger" + title={`Oops, something went wrong while trying to delete the selected ${modelsWord}. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`} + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const onConfirmDeleteModalDelete = useCallback( + async (workspaceDescriptors: WorkspaceDescriptor[]) => { + const modelsWord = selectedWorkspaceIds.length > 1 ? "Models" : "Model"; + setIsConfirmDeleteModalOpen(false); + + Promise.all( + workspaceDescriptors + .filter((w) => selectedWorkspaceIds.includes(w.workspaceId)) + .map((w) => workspaces.deleteWorkspace(w)) + ) + .then(() => { + deleteSuccessAlert.show({ modelsWord }); + }) + .catch((e) => { + console.error(e); + deleteErrorAlert.show({ modelsWord }); + }) + .finally(() => { + setSelectedWorkspaceIds([]); + }); + }, + [selectedWorkspaceIds, workspaces, deleteErrorAlert, deleteSuccessAlert] + ); + + const onWsToggle = useCallback((workspaceId: WorkspaceDescriptor["workspaceId"], checked: boolean) => { + setSelectedWorkspaceIds((prevSelected) => { + const otherSelectedIds = prevSelected.filter((r) => r !== workspaceId); + return checked ? [...otherSelectedIds, workspaceId] : otherSelectedIds; + }); + }, []); + + const onToggleAllElements = useCallback((checked: boolean, workspaceDescriptors: WorkspaceDescriptor[]) => { + setSelectedWorkspaceIds(checked ? workspaceDescriptors.map((e) => e.workspaceId) : []); + }, []); + + const onClearFilters = useCallback(() => { + setSearchValue(""); + }, []); + + const isWsFolder = useCallback( + async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { + const { editableFiles, readonlyFiles } = splitFiles(await workspaces.getFiles({ workspaceId })); + return editableFiles.length > 1 || readonlyFiles.length > 0; + }, + [workspaces] + ); + + const getWorkspaceName = useCallback( + async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { + if (selectedWorkspaceIds.length !== 1) { + return ""; + } + const workspaceData = await workspaces.getWorkspace({ workspaceId }); + return (await isWsFolder(workspaceId)) + ? workspaceData.name + : (await workspaces.getFiles({ workspaceId }))[0].nameWithoutExtension; + }, + [isWsFolder, selectedWorkspaceIds, workspaces] + ); + + useEffect(() => { + Promise.all([ + Promise.all(selectedWorkspaceIds.map(isWsFolder)).then((results) => { + const foldersCount = results.filter((r) => r).length; + setSelectedFoldersCount(foldersCount); + }), + getWorkspaceName(selectedWorkspaceIds[0]).then(setFirstSelectedWorkspaceName), + ]) + .then(() => setDeleteModalDataLoaded(true)) + .catch(() => setDeleteModalFetchError(true)); + }, [getWorkspaceName, selectedWorkspaceIds, isWsFolder]); + + useEffect(() => { + setPageTitle([PAGE_TITLE]); + }, []); + + return ( + <PromiseStateWrapper + promise={workspaceDescriptorsPromise} + rejected={(e) => <>Error fetching workspaces: {e + ""}</>} + resolved={(workspaceDescriptors: WorkspaceDescriptor[]) => { + const itemCount = workspaceDescriptors.length; + + return ( + <> - <Alerts ref={alertsRef} width={"500px"} /> + <Page> + <PageSection variant={"light"}> + <TextContent> + <Text component={TextVariants.h1}>{PAGE_TITLE}</Text> + <Text component={TextVariants.p}> + Use your recent models from GitHub Repository, a GitHub Gist or saved in your browser. + </Text> + </TextContent> + </PageSection> + + <PageSection isFilled aria-label="workspaces-table-section"> + <PageSection variant={"light"} padding={{ default: "noPadding" }}> + {itemCount > 0 && ( + <> + <TableToolbar + itemCount={itemCount} + onDeleteActionButtonClick={() => setIsConfirmDeleteModalOpen(true)} + onToggleAllElements={(checked) => onToggleAllElements(checked, workspaceDescriptors)} + searchValue={searchValue} + selectedElementsCount={selectedWorkspaceIds.length} + setSearchValue={setSearchValue} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + /> + <WorkspacesTable + page={page} + perPage={perPage} + onClearFilters={onClearFilters} + onWsToggle={onWsToggle} + searchValue={searchValue} + selectedWorkspaceIds={selectedWorkspaceIds} + workspaceDescriptors={workspaceDescriptors} + /> + <TablePagination + itemCount={itemCount} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + variant="bottom" + /> + </> + )} + {workspaceDescriptors.length === 0 && ( + <Bullseye> + <EmptyState> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="lg"> + {`Nothing here`} + </Title> + <EmptyStateBody>{`Start by adding a new model`}</EmptyStateBody> + </EmptyState> + </Bullseye> + )} + </PageSection> + </PageSection> + </Page> + <ConfirmDeleteModal + isOpen={isConfirmDeleteModalOpen} + onClose={onConfirmDeleteModalClose} + onDelete={() => onConfirmDeleteModalDelete(workspaceDescriptors)} + elementsTypeName={selectedElementTypesName} + deleteMessage={deleteModalMessage} + dataLoaded={deleteModalDataLoaded} + fetchError={deleteModalFetchError} + /> + </> + ); + }} + /> + ); +} + +const capitalizeString = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); diff --cc packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx index 9829c9dd3f,0000000000..01438e496f mode 100644,000000..100644 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx @@@ -1,307 -1,0 +1,302 @@@ +/* + * 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 { PromiseStateWrapper } from "@kie-tools-core/react-hooks/dist/PromiseState"; +import { useController } from "@kie-tools-core/react-hooks/dist/useController"; +import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; +import { useWorkspacePromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks"; +import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace"; +import { Breadcrumb } from "@patternfly/react-core/components/Breadcrumb"; +import { BreadcrumbItem, Checkbox, Dropdown, DropdownToggle, ToolbarItem } from "@patternfly/react-core/dist/js"; +import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert"; +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 { Title } from "@patternfly/react-core/dist/js/components/Title"; +import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye"; +import { CaretDownIcon, PlusIcon } from "@patternfly/react-icons/dist/js/icons"; +import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; +import * as React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useHistory } from "react-router"; - import { Alerts, AlertsController, useAlert } from "../../../alerts/Alerts"; ++import { useGlobalAlert } from "../../../alerts/GlobalAlertsContext"; +import { NewFileDropdownMenu } from "../../../editor/NewFileDropdownMenu"; +import { splitFiles } from "../../../extension"; +import { routes } from "../../../navigation/Routes"; +import { setPageTitle } from "../../../PageTitle"; +import { ConfirmDeleteModal } from "../../../table/ConfirmDeleteModal"; +import { defaultPerPageOptions, TablePagination } from "../../../table/TablePagination"; +import { TableToolbar } from "../../../table/TableToolbar"; +import { WorkspaceFilesTable } from "./WorkspaceFilesTable"; + +export interface Props { + workspaceId: string; +} + +export function WorkspaceFiles(props: Props) { + const { workspaceId } = props; + const workspacePromise = useWorkspacePromise(workspaceId); + const [selectedWorkspaceFiles, setSelectedWorkspaceFiles] = useState<WorkspaceFile[]>([]); + const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); + const [searchValue, setSearchValue] = React.useState(""); + const [page, setPage] = React.useState(1); + const [perPage, setPerPage] = React.useState(5); + const [isViewRoFilesChecked, setIsViewRoFilesChecked] = useState(false); + const [isNewFileDropdownMenuOpen, setNewFileDropdownMenuOpen] = useState(false); + const workspaces = useWorkspaces(); + const history = useHistory(); - const [alerts, alertsRef] = useController<AlertsController>(); + const isSelectedWorkspaceFilesPlural = useMemo(() => selectedWorkspaceFiles.length > 1, [selectedWorkspaceFiles]); + const selectedElementTypesName = useMemo( + () => (isSelectedWorkspaceFilesPlural ? "files" : "file"), + [isSelectedWorkspaceFilesPlural] + ); + + const deleteModalMessage = useMemo( + () => ( + <> + Deleting {isSelectedWorkspaceFilesPlural ? "these" : "this"}{" "} + <b>{isSelectedWorkspaceFilesPlural ? selectedWorkspaceFiles.length : selectedWorkspaceFiles[0]?.name}</b>{" "} + {selectedElementTypesName} + </> + ), + [isSelectedWorkspaceFilesPlural, selectedWorkspaceFiles, selectedElementTypesName] + ); + + const onConfirmDeleteModalClose = useCallback(() => setIsConfirmDeleteModalOpen(false), []); + - const deleteSuccessAlert = useAlert<{ selectedElementTypesName: string }>( - alerts, ++ const deleteSuccessAlert = useGlobalAlert<{ selectedElementTypesName: string }>( + useCallback(({ close }, { selectedElementTypesName }) => { + return <Alert variant="success" title={`${capitalizeString(selectedElementTypesName)} deleted successfully`} />; + }, []), + { durationInSeconds: 2 } + ); + - const deleteErrorAlert = useAlert<{ selectedElementTypesName: string }>( - alerts, ++ const deleteErrorAlert = useGlobalAlert<{ selectedElementTypesName: string }>( + useCallback(({ close }, { selectedElementTypesName }) => { + return ( + <Alert + variant="danger" + title={`Oops, something went wrong while trying to delete the selected ${selectedElementTypesName}. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`} + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const onConfirmDeleteModalDelete = useCallback( + async (totalFilesCount: number) => { + setIsConfirmDeleteModalOpen(false); + + if (selectedWorkspaceFiles.length === totalFilesCount) { + workspaces.deleteWorkspace({ workspaceId }); + history.push({ pathname: routes.recentModels.path({}) }); + deleteSuccessAlert.show({ selectedElementTypesName }); + return; + } + + Promise.all(selectedWorkspaceFiles.map((file) => workspaces.deleteFile({ file }))) + .then(() => { + deleteSuccessAlert.show({ selectedElementTypesName }); + }) + .catch((e) => { + console.error(e); + deleteErrorAlert.show({ selectedElementTypesName }); + }) + .finally(() => { + setSelectedWorkspaceFiles([]); + }); + }, + [ + selectedWorkspaceFiles, + workspaces, + history, + workspaceId, + deleteErrorAlert, + deleteSuccessAlert, + selectedElementTypesName, + ] + ); + + const onFileToggle = useCallback((workspaceFile: WorkspaceFile, checked: boolean) => { + setSelectedWorkspaceFiles((prevSelected) => { + const otherSelectedFiles = [...prevSelected.filter((f) => f !== workspaceFile)]; + return checked ? [...otherSelectedFiles, workspaceFile] : otherSelectedFiles; + }); + }, []); + + const onToggleAllElements = useCallback((checked: boolean, files: WorkspaceFile[]) => { + setSelectedWorkspaceFiles(checked ? files : []); + }, []); + + const handleViewRoCheckboxChange = useCallback((checked: boolean) => { + setIsViewRoFilesChecked(checked); + }, []); + + useEffect(() => { + setSelectedWorkspaceFiles([]); + }, [workspacePromise]); + + return ( + <PromiseStateWrapper + promise={workspacePromise} + rejected={(e) => <>Error fetching workspaces: {e + ""}</>} + resolved={(workspace: ActiveWorkspace) => { + const allFiles = splitFiles(workspace.files); + const isViewRoFilesDisabled = !allFiles.editableFiles.length || !allFiles.readonlyFiles.length; + const isViewRoFilesCheckedInternal = isViewRoFilesDisabled ? true : isViewRoFilesChecked; + const files = [...allFiles.editableFiles, ...(isViewRoFilesCheckedInternal ? allFiles.readonlyFiles : [])]; + const filesCount = files.length; + const allFilesCount = workspace.files.length; + + setPageTitle([workspace.descriptor.name]); + + return ( + <> - <Alerts ref={alertsRef} width={"500px"} /> + <Page + breadcrumb={ + <Breadcrumb> + <BreadcrumbItem to={"#" + routes.recentModels.path({})}>Recent Models</BreadcrumbItem> + <BreadcrumbItem to="#" isActive> + {workspace.descriptor.name} + </BreadcrumbItem> + </Breadcrumb> + } + > + <PageSection variant={"light"}> + <TextContent> + <Text component={TextVariants.h1}>Files in ‘{workspace.descriptor.name}’</Text> + <Text component={TextVariants.p}> + Use your recent models from GitHub Repository, a GitHub Gist or saved in your browser. + </Text> + </TextContent> + </PageSection> + + <PageSection isFilled aria-label="workspaces-table-section"> + <PageSection variant={"light"} padding={{ default: "noPadding" }}> + {filesCount > 0 && ( + <> + <TableToolbar + itemCount={filesCount} + onDeleteActionButtonClick={() => setIsConfirmDeleteModalOpen(true)} + onToggleAllElements={(checked) => onToggleAllElements(checked, files)} + searchValue={searchValue} + selectedElementsCount={selectedWorkspaceFiles.length} + setSearchValue={setSearchValue} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + additionalComponents={ + <> + <ToolbarItem> + <Dropdown + position={"right"} + isOpen={isNewFileDropdownMenuOpen} + toggle={ + <DropdownToggle + onToggle={setNewFileDropdownMenuOpen} + toggleIndicator={CaretDownIcon} + toggleVariant="primary" + > + <PlusIcon /> + New file + </DropdownToggle> + } + > + <NewFileDropdownMenu - alerts={alerts} + workspaceId={workspaceId} + destinationDirPath={""} + onAddFile={async (file) => { + setNewFileDropdownMenuOpen(false); + if (!file) { + return; + } + + history.push({ + pathname: routes.workspaceWithFilePath.path({ + workspaceId: file.workspaceId, + fileRelativePath: file.relativePathWithoutExtension, + extension: file.extension, + }), + }); + }} + /> + </Dropdown> + </ToolbarItem> + <ToolbarItem> + <Checkbox + id="viewRoFiles" + label="View readonly files" + isChecked={isViewRoFilesCheckedInternal} + isDisabled={isViewRoFilesDisabled} + onChange={handleViewRoCheckboxChange} + ></Checkbox> + </ToolbarItem> + </> + } + /> + + <WorkspaceFilesTable + page={page} + perPage={perPage} + onFileToggle={onFileToggle} + searchValue={searchValue} + selectedWorkspaceFiles={selectedWorkspaceFiles} + totalFilesCount={allFilesCount} + workspaceFiles={files} + /> + + <TablePagination + itemCount={filesCount} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + variant="bottom" + /> + </> + )} + {files.length === 0 && ( + <Bullseye> + <EmptyState> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="lg"> + {`Nothing here`} + </Title> + <EmptyStateBody>{`Start by adding a new model`}</EmptyStateBody> + </EmptyState> + </Bullseye> + )} + </PageSection> + </PageSection> + </Page> + <ConfirmDeleteModal + isOpen={isConfirmDeleteModalOpen} + onClose={onConfirmDeleteModalClose} + onDelete={() => onConfirmDeleteModalDelete(workspace.files.length)} + elementsTypeName={selectedElementTypesName} + deleteMessage={deleteModalMessage} + /> + </> + ); + }} + /> + ); +} + +const capitalizeString = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); diff --cc packages/serverless-logic-web-tools/src/openshift/dropdown/OpenshiftDeploymentsDropdown.tsx index ae3746a328,9ec9127921..caa87e3bde --- a/packages/serverless-logic-web-tools/src/openshift/dropdown/OpenshiftDeploymentsDropdown.tsx +++ b/packages/serverless-logic-web-tools/src/openshift/dropdown/OpenshiftDeploymentsDropdown.tsx @@@ -29,14 -29,28 +29,30 @@@ import { useSettings, useSettingsDispat import { useOpenShift } from "../OpenShiftContext"; import { OpenShiftDeploymentDropdownItem } from "./OpenShiftDeploymentDropdownItem"; import { OpenShiftInstanceStatus } from "../OpenShiftInstanceStatus"; + import { WebToolsOpenShiftDeployedModel } from "../deploy/types"; + import { useEnv } from "../../env/EnvContext"; + import { PromiseStateStatus, useLivePromiseState } from "@kie-tools-core/react-hooks/dist/PromiseState"; + import { useDevModeDispatch } from "../swfDevMode/DevModeContext"; + import { Skeleton } from "@patternfly/react-core/dist/js/components/Skeleton"; + import { Holder } from "@kie-tools-core/react-hooks/dist/Holder"; + import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex"; + import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; + import { Divider } from "@patternfly/react-core/dist/js/components/Divider"; +import { routes } from "../../navigation/Routes"; +import { useHistory } from "react-router"; + const REFRESH_COUNTDOWN_INITIAL_VALUE_IN_SECONDS = 20; + export function OpenshiftDeploymentsDropdown() { + const { env } = useEnv(); const settings = useSettings(); const settingsDispatch = useSettingsDispatch(); const openshift = useOpenShift(); + const devModeDispatch = useDevModeDispatch(); + const [refreshCountdownInSeconds, setRefreshCountdownInSeconds] = useState( + REFRESH_COUNTDOWN_INITIAL_VALUE_IN_SECONDS + ); + const history = useHistory(); const isConnected = useMemo( () => settings.openshift.status === OpenShiftInstanceStatus.CONNECTED, @@@ -44,28 -58,71 +60,71 @@@ ); const openOpenShiftSettings = useCallback(() => { - settingsDispatch.open(SettingsTabs.OPENSHIFT); - }, [settingsDispatch]); + history.push(routes.settings.openshift.path({})); + }, [history]); - const items = useMemo(() => { - const common = isConnected - ? [ - <DropdownItem - key={"dropdown-openshift-setup-as"} - component={"button"} - onClick={openOpenShiftSettings} - ouiaId={"setup-as-openshift-dropdown-button"} - description={"Change..."} - > - {`Connected to ${settings.openshift.config.namespace}`} - </DropdownItem>, - <DropdownSeparator key={"dropdown-openshift-separator-deployments-2"} />, - ] - : []; - - if (openshift.deployments.length === 0) { + const [deployments, refresh] = useLivePromiseState<WebToolsOpenShiftDeployedModel[]>( + useMemo(() => { + if (settings.openshift.status !== OpenShiftInstanceStatus.CONNECTED) { + return { error: "Can't load deployments." }; + } + return async () => { + setRefreshCountdownInSeconds(REFRESH_COUNTDOWN_INITIAL_VALUE_IN_SECONDS); + const res = await Promise.all([openshift.loadDeployments(), devModeDispatch.loadDeployments()]); + return res.flat(); + }; + }, [devModeDispatch, openshift, settings.openshift.status]) + ); + + useEffect(() => { + if (deployments.status === PromiseStateStatus.PENDING) { + return; + } + + const interval = window.setInterval(() => { + setRefreshCountdownInSeconds((prev) => prev - 1); + }, 1000); + + return () => { + window.clearInterval(interval); + }; + }, [deployments.status]); + + useEffect(() => { + if (refreshCountdownInSeconds > 0) { + return; + } + refresh(new Holder(false)); + }, [refresh, refreshCountdownInSeconds]); + + const connectionItem = useMemo( + () => [ + <DropdownItem + key={"dropdown-openshift-setup-as"} + component={"button"} + onClick={openOpenShiftSettings} + ouiaId={"setup-as-openshift-dropdown-button"} + description={"Change..."} + > + {`Connected to ${settings.openshift.config.namespace}`} + </DropdownItem>, + <DropdownSeparator key={"dropdown-openshift-separator-deployments-2"} />, + ], + [openOpenShiftSettings, settings.openshift.config.namespace] + ); + + const deploymentItems = useMemo(() => { + if (deployments.status === PromiseStateStatus.PENDING) { + return [ + Array.from({ length: 3 }, (_, idx) => ( + <DropdownItem key={`deployment-skeleton-${idx}`} isDisabled={true}> + <Skeleton width={"80%"} style={{ marginBottom: "4px" }} /> + <Skeleton width={"50%"} /> + </DropdownItem> + )), + ]; + } else if (deployments.status === PromiseStateStatus.REJECTED) { return [ - ...common, <DropdownItem key="disabled link" isDisabled> <Bullseye> <EmptyState> @@@ -78,30 -148,47 +150,54 @@@ </DropdownItem>, ]; } else { + const dropdownItems = []; + + const [devModeDeployments, userDeployments] = deployments.data + .sort((a, b) => b.creationTimestamp.getTime() - a.creationTimestamp.getTime()) + .reduce( + ([devModeDeployments, userDeployments], d: WebToolsOpenShiftDeployedModel) => + d.devMode ? [[...devModeDeployments, d], userDeployments] : [devModeDeployments, [...userDeployments, d]], + [[] as WebToolsOpenShiftDeployedModel[], [] as WebToolsOpenShiftDeployedModel[]] + ); + + if (devModeDeployments.length > 0) { + dropdownItems.push( + <OpenShiftDeploymentDropdownItem + key={devModeDeployments[0].creationTimestamp.getTime()} + id={0} + deployment={devModeDeployments[0]} + refreshDeployments={refresh} + /> + ); + + if (userDeployments.length > 0) { + dropdownItems.push(<DropdownSeparator key={"dropdown-openshift-separator-deployments-2"} />); + } + } + return [ - ...common, - openshift.deployments - .sort((a, b) => b.creationTimestamp.getTime() - a.creationTimestamp.getTime()) - .map((deployment, i) => { - return ( - <OpenShiftDeploymentDropdownItem - key={deployment.creationTimestamp.getTime()} - id={i} - deployment={deployment} - /> - ); - }), + ...dropdownItems, + userDeployments.map((deployment, i) => { + return ( + <OpenShiftDeploymentDropdownItem + key={deployment.creationTimestamp.getTime()} + id={i + 1} + deployment={deployment} + refreshDeployments={refresh} + /> + ); + }), ]; } - }, [openshift.deployments, isConnected, openOpenShiftSettings, settings.openshift.config.namespace]); + }, [deployments.data, deployments.status, refresh]); + const onDeploymensDropdownToggle = useCallback(() => { + if (!isConnected) { + history.push(routes.settings.openshift.path({})); + } + openshift.setDeploymentsDropdownOpen((dropdownOpen) => isConnected && !dropdownOpen); + }, [history, isConnected, openshift]); + return ( <> <Tooltip diff --cc packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx index 10a8856bba,18497e2099..d9eaaef0ae --- a/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx +++ b/packages/serverless-logic-web-tools/src/settings/SettingsContext.tsx @@@ -14,23 -14,30 +14,24 @@@ * limitations under the License. */ +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 { SwfServiceCatalogStore } from "../editor/api/SwfServiceCatalogStore"; +import { useKieSandboxExtendedServices } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; + import { readDevModeEnabledConfigCookie, readOpenShiftConfigCookie } from "./openshift/OpenShiftSettingsConfig"; -import { isKubernetesConnectionValid } from "@kie-tools-core/kubernetes-bridge/dist/service"; import { OpenShiftInstanceStatus } from "../openshift/OpenShiftInstanceStatus"; import { OpenShiftService } from "@kie-tools-core/kubernetes-bridge/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 { 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"; + import { useEnv } from "../env/EnvContext"; import { KubernetesConnection } from "@kie-tools-core/kubernetes-bridge/dist/service"; export enum AuthStatus { @@@ -117,6 -129,32 +120,7 @@@ export const SettingsContext = React.cr export const SettingsDispatchContext = React.createContext<SettingsDispatchContextType>({} as any); export function SettingsContextProvider(props: any) { - const queryParams = useQueryParams(); - const history = useHistory(); + const { env } = useEnv(); - 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()); @@@ -272,8 -314,11 +274,9 @@@ }, }; }, [ - isOpen, - activeTab, openshiftStatus, openshiftConfig, + isOpenShiftDevModeEnabled, githubAuthStatus, githubToken, githubUser, diff --cc packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettings.tsx index 70e28a0b6e,0000000000..5768ffdbed mode 100644,000000..100644 --- a/packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettings.tsx +++ b/packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettings.tsx @@@ -1,163 -1,0 +1,170 @@@ +/* + * 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 { KubernetesConnection } from "@kie-tools-core/kubernetes-bridge/dist/service"; +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 { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal"; +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/dist/js/icons/add-circle-o-icon"; +import { CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons/check-circle-icon"; - import { useCallback, useEffect, useState } from "react"; ++import { useCallback, useEffect, useState, useMemo } from "react"; +import { Link } from "react-router-dom"; +import { SETTINGS_PAGE_SECTION_TITLE } from "../SettingsContext"; +import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; +import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; +import { routes } from "../../navigation/Routes"; +import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; +import { setPageTitle } from "../../PageTitle"; +import { obfuscate } from "../github/GitHubSettings"; +import { useSettings, useSettingsDispatch } from "../SettingsContext"; +import { SettingsPageProps } from "../types"; +import { saveConfigCookie } from "./OpenShiftSettingsConfig"; +import { OpenShiftSettingsSimpleConfig } from "./OpenShiftSettingsSimpleConfig"; + +const PAGE_TITLE = "OpenShift"; + +export function OpenShiftSettings(props: SettingsPageProps) { + 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: KubernetesConnection = { + namespace: settings.openshift.config.namespace, + host: settings.openshift.config.host, + token: "", + }; + settingsDispatch.openshift.setConfig(newConfig); + saveConfigCookie(newConfig); + }, [settings.openshift.config, settingsDispatch.openshift]); + ++ const devModeEnabledLabel = useMemo( ++ () => (settings.openshift.isDevModeEnabled ? "enabled" : "disabled"), ++ [settings.openshift.isDevModeEnabled] ++ ); ++ + useEffect(() => { + setPageTitle([SETTINGS_PAGE_SECTION_TITLE, PAGE_TITLE]); + }, []); + + return ( + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>{PAGE_TITLE}</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 /> ++ Uploading models to Dev Mode is <b>{devModeEnabledLabel}</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> + + {props.pageContainerRef.current && ( + <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} + appendTo={props.pageContainerRef.current || document.body} + > + <OpenShiftSettingsSimpleConfig /> + </Modal> + )} + </Page> + ); +} diff --cc packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettingsSimpleConfig.tsx index c3de7692a6,b4e4d3d280..e9333adc72 --- a/packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettingsSimpleConfig.tsx +++ b/packages/serverless-logic-web-tools/src/settings/openshift/OpenShiftSettingsSimpleConfig.tsx @@@ -23,23 -24,21 +23,25 @@@ import { Text } from "@patternfly/react 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 { useCallback, useEffect, useState } from "react"; +import * as React from "react"; +import { useCallback, useEffect, useState, useContext } from "react"; +import { Link } from "react-router-dom"; import { useAppI18n } from "../../i18n"; + import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; + import { EMPTY_CONFIG, saveConfigCookie, saveDevModeEnabledConfigCookie } from "./OpenShiftSettingsConfig"; import { isKubernetesConnectionValid, KubernetesConnection, KubernetesConnectionStatus, } from "@kie-tools-core/kubernetes-bridge/dist/service"; + import { useSettings, useSettingsDispatch } from "../SettingsContext"; import { useKieSandboxExtendedServices } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesContext"; import { KieSandboxExtendedServicesStatus } from "../../kieSandboxExtendedServices/KieSandboxExtendedServicesStatus"; -import { SettingsTabs } from "../SettingsModalBody"; + import { Checkbox } from "@patternfly/react-core/dist/js/components/Checkbox"; + import { DEV_MODE_FEATURE_NAME } from "../../openshift/swfDevMode/DevModeConstants"; +import { routes } from "../../navigation/Routes"; - import { OpenShiftInstanceStatus } from "../../openshift/OpenShiftInstanceStatus"; +import { QuickStartIds } from "../../quickstarts-data"; - import { useSettings, useSettingsDispatch } from "../SettingsContext"; - import { EMPTY_CONFIG, saveConfigCookie } from "./OpenShiftSettingsConfig"; +import { QuickStartContext, QuickStartContextValues } from "@patternfly/quickstarts"; enum FormValiationOptions { INITIAL = "INITIAL", @@@ -56,7 -55,7 +58,8 @@@ export function OpenShiftSettingsSimple const [isConfigValidated, setConfigValidated] = useState(FormValiationOptions.INITIAL); const [isConnecting, setConnecting] = useState(false); const kieSandboxExtendedServices = useKieSandboxExtendedServices(); + const [isDevModeConfigEnabled, setDevModeConfigEnabled] = useState(settings.openshift.isDevModeEnabled); + const qsContext = useContext<QuickStartContextValues>(QuickStartContext); useEffect(() => { setConfig(settings.openshift.config); @@@ -306,19 -326,39 +318,51 @@@ </Button> </InputGroupText> </InputGroup> + <br /> + <Button + isInline={true} + key="quickstart" + variant="link" + onClick={() => { + qsContext.setActiveQuickStartID?.(QuickStartIds.OpenShiftIntegrationQuickStart); + setTimeout(() => qsContext.setQuickStartTaskNumber?.(QuickStartIds.OpenShiftIntegrationQuickStart, 1), 0); + }} + > + Need help getting started? Follow our quickstart guide. + </Button> </FormGroup> + <FormGroup + label={DEV_MODE_FEATURE_NAME} + labelIcon={ + <Popover + bodyContent={ + "Automatically spins up a deployment running Quarkus in dev mode, making it quick to try model changes" + } + > + <button + type="button" + aria-label="More info for Dev Mode field" + onClick={(e) => e.preventDefault()} + aria-describedby="dev-mode-field" + className="pf-c-form__group-label-help" + > + <HelpIcon noVerticalAlign /> + </button> + </Popover> + } + isRequired + fieldId="dev-mode-field" + > + <Checkbox + id="enable-dev-mode-checkbox" + label="Enable Dev Mode" + description={ + "Be sure to set up at least 4GB of ram for your OpenShift deployments, otherwise, the Dev Mode deployment may run into issues." + } + isChecked={isDevModeConfigEnabled} + onChange={onEnableDevModeConfigChanged} + /> + </FormGroup> <ActionGroup> <Button id="openshift-config-save-button" diff --cc packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx index ccba6c851f,0000000000..4996d1f571 mode 100644,000000..100644 --- a/packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx +++ b/packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx @@@ -1,199 -1,0 +1,195 @@@ +/* + * 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 { useController } from "@kie-tools-core/react-hooks/dist/useController"; +import { Alert, AlertActionCloseButton, Button } from "@patternfly/react-core/dist/js"; +import { Checkbox } from "@patternfly/react-core/dist/js/components/Checkbox"; +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 { Alerts, AlertsController, useAlert } from "../../alerts/Alerts"; +import { APP_NAME } from "../../AppConstants"; +import { routes } from "../../navigation/Routes"; +import { setPageTitle } from "../../PageTitle"; +import { ConfirmDeleteModal } from "../../table/ConfirmDeleteModal"; +import { SETTINGS_PAGE_SECTION_TITLE } from "../SettingsContext"; +import { deleteAllCookies } from "../../cookies"; +import { isBrowserChromiumBased } from "../../workspace/startupBlockers/SupportedBrowsers"; +import { useHistory } from "react-router"; ++import { useGlobalAlert } from "../../alerts/GlobalAlertsContext"; + +const PAGE_TITLE = "Storage"; +/** + * delete alert delay in seconds before reloading the app. + */ +const DELETE_ALERT_DELAY = 5; + +/** + * Delete all indexed DBs + */ +const deleteAllIndexedDBs = async () => { + Promise.all( + (await indexedDB.databases()).filter((db) => db.name).map(async (db) => indexedDB.deleteDatabase(db.name!)) + ); +}; + +function Timer(props: { delay: number }) { + const [delay, setDelay] = useState(props.delay); + + useEffect(() => { + const timer = setInterval(() => { + setDelay((prevDelay) => prevDelay - 1); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, []); + + return <>{delay}</>; +} + +export function StorageSettings() { + const [isDeleteCookiesChecked, setIsDeleteCookiesChecked] = useState(false); + const [isDeleteLocalStorageChecked, setIsDeleteLocalStorageChecked] = useState(false); + const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); - const [alerts, alertsRef] = useController<AlertsController>(); + const history = useHistory(); + + const toggleConfirmModal = useCallback(() => { + setIsConfirmDeleteModalOpen((isOpen) => !isOpen); + }, []); + - const deleteSuccessAlert = useAlert( - alerts, ++ const deleteSuccessAlert = useGlobalAlert( + useCallback(({ close }) => { + setTimeout(() => { + window.location.href = window.location.origin + window.location.pathname; + }, DELETE_ALERT_DELAY * 1000); + return ( + <Alert + variant="success" + title={ + <> + Data deleted successfully. <br /> + You will be redirected to the home page in <Timer delay={DELETE_ALERT_DELAY} /> seconds + </> + } + /> + ); + }, []) + ); + - const deleteErrorAlert = useAlert( - alerts, ++ const deleteErrorAlert = useGlobalAlert( + useCallback(({ close }) => { + return ( + <Alert + variant="danger" + title={`Oops, something went wrong while trying to delete the selected data. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`} + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const onConfirmDeleteModalDelete = useCallback(async () => { + toggleConfirmModal(); + + try { + await deleteAllIndexedDBs(); + if (isDeleteLocalStorageChecked) { + localStorage.clear(); + } + if (isDeleteCookiesChecked) { + deleteAllCookies(); + } + deleteSuccessAlert.show(); + } catch (e) { + deleteErrorAlert.show(); + } + }, [toggleConfirmModal, deleteSuccessAlert, isDeleteLocalStorageChecked, deleteErrorAlert, isDeleteCookiesChecked]); + + useEffect(() => { + if (!isBrowserChromiumBased()) { + history.replace(routes.settings.home.path({})); + } + setPageTitle([SETTINGS_PAGE_SECTION_TITLE, PAGE_TITLE]); + }, [history]); + + return ( + <> - <Alerts ref={alertsRef} width={"500px"} /> + <Page> + <PageSection variant={"light"} isWidthLimited> + <TextContent> + <Text component={TextVariants.h1}>{PAGE_TITLE}</Text> + <Text component={TextVariants.p}> + Here, you have the ability to completely erase all stored data in your browser. + <br /> + Safely delete your cookies, modules, settings and all information locally stored in your browser, giving a + fresh start to {APP_NAME}. + </Text> + </TextContent> + </PageSection> + + <PageSection> + <PageSection variant={"light"}> + <Form> + <Checkbox + id="delete-indexedDB" + label="Storage" + description={"Delete all databases. You will lose all your modules and workspaces."} + isChecked + isDisabled + /> + <Alert + variant="warning" + isInline + title="By selecting the cookies and local storage, all your saved settings will be permanently erased." + > + <br /> + <Checkbox + id="delete-cookies" + label="Cookies" + description={"Delete all cookies."} + isChecked={isDeleteCookiesChecked} + onChange={setIsDeleteCookiesChecked} + /> + <br /> + <Checkbox + id="delete-localStorage" + label="LocalStorage" + description={"Delete all localStorage information."} + isChecked={isDeleteLocalStorageChecked} + onChange={setIsDeleteLocalStorageChecked} + /> + </Alert> + </Form> + <br /> + <Button variant="danger" onClick={toggleConfirmModal}> + Delete data + </Button> + </PageSection> + </PageSection> + <ConfirmDeleteModal + isOpen={isConfirmDeleteModalOpen} + onClose={toggleConfirmModal} + onDelete={onConfirmDeleteModalDelete} + elementsTypeName="data" + deleteMessage="By deleting this data will permanently erase your stored information." + /> + </Page> + </> + ); +} diff --cc packages/serverless-logic-web-tools/src/workspace/components/NewWorkspaceFromSample.tsx index 500a06bd09,1c1b81bb4c..1083f71a90 --- a/packages/serverless-logic-web-tools/src/workspace/components/NewWorkspaceFromSample.tsx +++ b/packages/serverless-logic-web-tools/src/workspace/components/NewWorkspaceFromSample.tsx @@@ -24,9 -24,10 +24,9 @@@ import { Spinner } from "@patternfly/re import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; import { QueryParams } from "../../navigation/Routes"; import { useQueryParam } from "../../queryParams/QueryParamsContext"; -import { OnlineEditorPage } from "../../pageTemplate/OnlineEditorPage"; import { PageSection } from "@patternfly/react-core/dist/js/components/Page"; import { EditorPageErrorPage } from "../../editor/EditorPageErrorPage"; - import { KIE_SAMPLES_REPO } from "../../home/sample/sampleApi"; + import { KIE_SAMPLES_REPO } from "../../home/sample/SampleApi"; import { useSampleDispatch } from "../../home/sample/hooks/SampleContext"; export function NewWorkspaceFromSample() { diff --cc pnpm-lock.yaml index e676671d44,541f19f201..4c0ea5231d --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@@ -6101,9 -6120,12 +6126,15 @@@ importers react-router-dom: specifier: ^5.2.1 version: 5.3.0([email protected]) + short-unique-id: + specifier: ^4.4.4 + version: 4.4.4 + showdown: + specifier: ^2.1.0 + version: 2.1.0 + uuid: + specifier: ^8.3.2 + version: 8.3.2 vscode-languageserver-types: specifier: ^3.16.0 version: 3.17.2 @@@ -31829,14 -31734,12 +31777,20 @@@ packages dev: true optional: true + /[email protected]: + resolution: + { integrity: sha512-oLF1NCmtbiTWl2SqdXZQbo5KM1b7axdp0RgQLq8qCBBLoq+o3A5wmLrNM6bZIh54/a8BJ3l69kTXuxwZ+XCYuw== } + hasBin: true + dev: false + + /[email protected]: + resolution: + { integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== } + hasBin: true + dependencies: + commander: 9.4.1 + dev: false + /[email protected]: resolution: { integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
