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 9335ac938501e5b7e980216f9fa504739afe953c Author: Fabrizio Antonangeli <[email protected]> AuthorDate: Tue May 23 15:25:21 2023 +0200 KOGITO-8966: Ability to completely erase stored data on browser (#1656) Co-authored-by: Guilherme Caponetto <[email protected]> --- .../src/cookies/index.ts | 9 + .../src/home/sample/SamplesCatalog.tsx | 14 +- .../src/homepage/pageTemplate/OnlineEditorPage.tsx | 6 +- .../src/navigation/Routes.ts | 1 + .../src/settings/routes/SettingsPageRoutes.tsx | 4 + .../src/settings/storage/StorageSettings.tsx | 199 +++++++++++++++++++++ .../src/settings/uiNav/SettingsPageNav.tsx | 15 +- .../workspace/startupBlockers/SupportedBrowsers.ts | 8 + 8 files changed, 247 insertions(+), 9 deletions(-) diff --git a/packages/serverless-logic-web-tools/src/cookies/index.ts b/packages/serverless-logic-web-tools/src/cookies/index.ts index 73c7f62f1f..3776e20e81 100644 --- a/packages/serverless-logic-web-tools/src/cookies/index.ts +++ b/packages/serverless-logic-web-tools/src/cookies/index.ts @@ -32,3 +32,12 @@ export function setCookie(name: string, value: string) { } export const makeCookieName = (group: string, name: string) => `KIE-TOOLS__serverless-logic-sandbox__${group}--${name}`; + +/** + * Delete all cookies + */ +export const deleteAllCookies = () => { + document.cookie.split(";").forEach(function (c) { + document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); + }); +}; diff --git a/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx b/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx index cac108bc89..76258ddfb2 100644 --- a/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx +++ b/packages/serverless-logic-web-tools/src/home/sample/SamplesCatalog.tsx @@ -75,7 +75,7 @@ export function SamplesCatalog() { const [sampleCovers, setSampleCovers] = useState<SampleCoversHashtable>({}); const [sampleLoadingError, setSampleLoadingError] = useState(""); const [searchFilter, setSearchFilter] = useState(""); - const [searchParams, setSearchParams] = useState<SearchParams>({ searchValue: "", category: undefined }); + const [searchParams, setSearchParams] = useState<SearchParams | undefined>(undefined); const [page, setPage] = React.useState(1); const [isCategoryFilterDropdownOpen, setCategoryFilterDropdownOpen] = useState(false); const history = useHistory(); @@ -121,12 +121,13 @@ export function SamplesCatalog() { const onSearch = useCallback( async (args: SearchParams) => { - if (args.searchValue === searchParams.searchValue && args.category === searchParams.category) { + 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] @@ -142,8 +143,13 @@ export function SamplesCatalog() { }, [categoryFilter, onSearch, searchFilter, setCategoryFilter]); useEffect(() => { + if (searchParams && searchFilter === searchParams.searchValue && categoryFilter === searchParams.category) { + return; + } + setSearchParams({ searchValue: searchFilter, category: categoryFilter }); + sampleDispatch - .getSamples({}) + .getSamples({ categoryFilter }) .then((data) => { const sortedSamples = data.sort( (a: Sample, b: Sample) => SAMPLE_PRIORITY[a.definition.category] - SAMPLE_PRIORITY[b.definition.category] @@ -156,7 +162,7 @@ export function SamplesCatalog() { .finally(() => { setLoading(false); }); - }, [sampleDispatch]); + }, [sampleDispatch, categoryFilter, searchFilter, searchParams]); useEffect(() => { sampleDispatch.getSampleCovers({ samples: visibleSamples, prevState: sampleCovers }).then(setSampleCovers); diff --git a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx index e37a20feb0..733cf4dd84 100644 --- a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx @@ -45,6 +45,7 @@ import { import { SettingsButton } from "../../settings/SettingsButton"; import { HomePageNav } from "../uiNav/HomePageNav"; import { APP_NAME } from "../../AppConstants"; +import { isBrowserChromiumBased } from "../../workspace/startupBlockers/SupportedBrowsers"; export type OnlineEditorPageProps = { children?: React.ReactNode; @@ -69,10 +70,7 @@ export function OnlineEditorPage(props: OnlineEditorPageProps) { useQueryParams: false, }; - const isChromiumBased = useMemo(() => { - const agent = window.navigator.userAgent.toLowerCase(); - return agent.indexOf("edg") > -1 || agent.indexOf("chrome") > -1; - }, []); + const isChromiumBased = useMemo(isBrowserChromiumBased, []); const headerToolbar = ( <Toolbar id="toolbar" isFullHeight isStatic> diff --git a/packages/serverless-logic-web-tools/src/navigation/Routes.ts b/packages/serverless-logic-web-tools/src/navigation/Routes.ts index e2e485fdf4..fb783a26a3 100644 --- a/packages/serverless-logic-web-tools/src/navigation/Routes.ts +++ b/packages/serverless-logic-web-tools/src/navigation/Routes.ts @@ -148,6 +148,7 @@ export const routes = { service_account: new Route<{}>(() => `${SETTINGS_ROUTE}/service-account`), service_registry: new Route<{}>(() => `${SETTINGS_ROUTE}/service-registry`), feature_preview: new Route<{}>(() => `${SETTINGS_ROUTE}/feature-preview`), + storage: new Route<{}>(() => `${SETTINGS_ROUTE}/storage`), }, static: { diff --git a/packages/serverless-logic-web-tools/src/settings/routes/SettingsPageRoutes.tsx b/packages/serverless-logic-web-tools/src/settings/routes/SettingsPageRoutes.tsx index ed3da455c8..78b029faea 100644 --- a/packages/serverless-logic-web-tools/src/settings/routes/SettingsPageRoutes.tsx +++ b/packages/serverless-logic-web-tools/src/settings/routes/SettingsPageRoutes.tsx @@ -25,6 +25,7 @@ import { OpenShiftSettings } from "../openshift/OpenShiftSettings"; import { ServiceAccountSettings } from "../serviceAccount/ServiceAccountSettings"; import { ServiceRegistrySettings } from "../serviceRegistry/ServiceRegistrySettings"; import { SettingsPageProps } from "../types"; +import { StorageSettings } from "../storage/StorageSettings"; export function SettingsPageRoutes(props: {} & SettingsPageProps) { const routes = useRoutes(); @@ -52,6 +53,9 @@ export function SettingsPageRoutes(props: {} & SettingsPageProps) { <Route path={routes.settings.feature_preview.path({})}> <FeaturePreviewSettings /> </Route> + <Route path={routes.settings.storage.path({})}> + <StorageSettings /> + </Route> <Route> <Redirect to={routes.settings.github.path({})} /> </Route> diff --git a/packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx b/packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx new file mode 100644 index 0000000000..ccba6c851f --- /dev/null +++ b/packages/serverless-logic-web-tools/src/settings/storage/StorageSettings.tsx @@ -0,0 +1,199 @@ +/* + * 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"; + +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, + 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, + 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 --git a/packages/serverless-logic-web-tools/src/settings/uiNav/SettingsPageNav.tsx b/packages/serverless-logic-web-tools/src/settings/uiNav/SettingsPageNav.tsx index 021cc91557..1673ea94b2 100644 --- a/packages/serverless-logic-web-tools/src/settings/uiNav/SettingsPageNav.tsx +++ b/packages/serverless-logic-web-tools/src/settings/uiNav/SettingsPageNav.tsx @@ -14,14 +14,18 @@ * limitations under the License. */ +import React from "react"; import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav"; -import * as React from "react"; +import { useMemo } from "react"; import { Link } from "react-router-dom"; import { useRoutes } from "../../navigation/Hooks"; +import { isBrowserChromiumBased } from "../../workspace/startupBlockers/SupportedBrowsers"; export function SettingsPageNav(props: { pathname: string }) { const routes = useRoutes(); + const isChromiumBased = useMemo(isBrowserChromiumBased, []); + return ( <> <div className="chr-c-app-title">Settings</div> @@ -65,6 +69,15 @@ export function SettingsPageNav(props: { pathname: string }) { > <Link to={routes.settings.feature_preview.path({})}>Feature Preview</Link> </NavItem> + {isChromiumBased && ( + <NavItem + itemId={0} + key={`Settings-storage-nav`} + isActive={props.pathname === routes.settings.storage.path({})} + > + <Link to={routes.settings.storage.path({})}>Storage</Link> + </NavItem> + )} </NavList> </Nav> </> diff --git a/packages/serverless-logic-web-tools/src/workspace/startupBlockers/SupportedBrowsers.ts b/packages/serverless-logic-web-tools/src/workspace/startupBlockers/SupportedBrowsers.ts index d264f58180..2fadacadf8 100644 --- a/packages/serverless-logic-web-tools/src/workspace/startupBlockers/SupportedBrowsers.ts +++ b/packages/serverless-logic-web-tools/src/workspace/startupBlockers/SupportedBrowsers.ts @@ -52,6 +52,14 @@ export const mapSupportedVersionsToBowser = (...features: MinVersionForFeature[] ); }; +/** + * Checks if the browser is Chromium based + */ +export const isBrowserChromiumBased = (): boolean => { + const agent = window.navigator.userAgent.toLowerCase(); + return agent.indexOf("edg") > -1 || agent.indexOf("chrome") > -1; +}; + export const SUPPORTED_BROWSERS = mapSupportedVersionsToBowser(SharedWebWorkersFeature, BroadcastChannelFeature); const IS_SUPPORTED = Bowser.getParser(window.navigator.userAgent).satisfies(SUPPORTED_BROWSERS); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
