diff --git a/chart/kubeapps/Chart.yaml b/chart/kubeapps/Chart.yaml index 3e176c82d89..3b2924d32a1 100644 --- a/chart/kubeapps/Chart.yaml +++ b/chart/kubeapps/Chart.yaml @@ -31,4 +31,4 @@ maintainers: name: kubeapps sources: - https://github.com/vmware-tanzu/kubeapps -version: 11.0.2-dev1 +version: 11.1.0-dev0 diff --git a/chart/kubeapps/README.md b/chart/kubeapps/README.md index b76250ec594..6d73903642c 100644 --- a/chart/kubeapps/README.md +++ b/chart/kubeapps/README.md @@ -421,12 +421,19 @@ Once you have installed Kubeapps follow the [Getting Started Guide](https://gith ### Other Parameters -| Name | Description | Value | -| ------------------------- | ----------------------------------------------------------------------------- | ------- | -| `allowNamespaceDiscovery` | Allow users to discover available namespaces (only the ones they have access) | `true` | -| `clusters` | List of clusters that Kubeapps can target for deployments | `[]` | -| `featureFlags.operators` | Enable ingress record generation for Kubeapps | `false` | -| `rbac.create` | Specifies whether RBAC resources should be created | `true` | +| Name | Description | Value | +| ------------------------- | ----------------------------------------------------------------------------- | ------ | +| `allowNamespaceDiscovery` | Allow users to discover available namespaces (only the ones they have access) | `true` | +| `clusters` | List of clusters that Kubeapps can target for deployments | `[]` | +| `rbac.create` | Specifies whether RBAC resources should be created | `true` | + + +### Feature flags + +| Name | Description | Value | +| --------------------------- | ---------------------------------------------------------- | ------- | +| `featureFlags.operators` | Enable support for Operators in Kubeapps | `false` | +| `featureFlags.schemaEditor` | Enable a visual editor for customizing the package schemas | `false` | ### Database Parameters diff --git a/chart/kubeapps/values.yaml b/chart/kubeapps/values.yaml index ebac5cc3035..05f7434442a 100644 --- a/chart/kubeapps/values.yaml +++ b/chart/kubeapps/values.yaml @@ -1417,13 +1417,6 @@ clusters: - name: default domain: cluster.local -## @skip featureFlags Feature flags (used to switch on development features) -## -featureFlags: - ## @param featureFlags.operators Enable ingress record generation for Kubeapps - ## - operators: false - ## RBAC configuration ## rbac: @@ -1431,6 +1424,17 @@ rbac: ## create: true +## @section Feature flags + +## Used enable some opt-in development features +## +featureFlags: + ## @param featureFlags.operators Enable support for Operators in Kubeapps + operators: false + ## @param featureFlags.schemaEditor Enable a visual editor for customizing the package schemas + ## + schemaEditor: false + ## @section Database Parameters ## PostgreSQL chart configuration diff --git a/dashboard/public/config.json b/dashboard/public/config.json index 77ae560364a..5f1176aaf0e 100644 --- a/dashboard/public/config.json +++ b/dashboard/public/config.json @@ -10,7 +10,8 @@ "oauthLogoutURI": "/oauth2/sign_out", "clusters": ["default"], "featureFlags": { - "operators": false + "operators": false, + "schemaEditor": false }, "theme": "light", "remoteComponentsUrl": "", diff --git a/dashboard/src/actions/config.test.tsx b/dashboard/src/actions/config.test.tsx index 43a90e705cf..b90f36d9e0d 100644 --- a/dashboard/src/actions/config.test.tsx +++ b/dashboard/src/actions/config.test.tsx @@ -22,7 +22,7 @@ const testConfig = { oauthLogoutURI: "", authProxySkipLoginPage: false, clusters: [], - featureFlags: { operators: false }, + featureFlags: { operators: false, schemaEditor: false }, theme: SupportedThemes.light, remoteComponentsUrl: "", customAppViews: [], diff --git a/dashboard/src/components/AppList/AppList.test.tsx b/dashboard/src/components/AppList/AppList.test.tsx index 88f10be8271..bfe4d500bfb 100644 --- a/dashboard/src/components/AppList/AppList.test.tsx +++ b/dashboard/src/components/AppList/AppList.test.tsx @@ -58,7 +58,7 @@ afterEach(() => { context("when changing props", () => { it("should fetch apps in the new namespace", async () => { const state = deepClone(initialState) as IStoreState; - state.config.featureFlags = { operators: true }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: true }; const store = getStore(state); const fetchInstalledPackages = jest.fn(); const getCustomResources = jest.fn(); @@ -71,7 +71,7 @@ context("when changing props", () => { it("should not fetch resources in the new namespace when operators is deactivated", async () => { const state = deepClone(initialState) as IStoreState; - state.config.featureFlags = { operators: false }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: false }; const store = getStore(state); const fetchInstalledPackages = jest.fn(); const getCustomResources = jest.fn(); @@ -110,7 +110,7 @@ context("when changing props", () => { it("should fetch apps in all namespaces", async () => { const state = deepClone(initialState) as IStoreState; - state.config.featureFlags = { operators: true }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: true }; const store = getStore(state); const fetchInstalledPackages = jest.fn(); const getCustomResources = jest.fn(); diff --git a/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx b/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx index 7acf275ad39..1a8c93695d1 100644 --- a/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx +++ b/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx @@ -27,7 +27,7 @@ const defaultProps = { }; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.installedpackages = { ...actions.installedpackages, @@ -38,7 +38,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx b/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx index 96560af9e7e..5933d4ff9b0 100644 --- a/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx +++ b/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx @@ -30,7 +30,7 @@ const defaultProps = { }; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.installedpackages = { ...actions.installedpackages, @@ -41,7 +41,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/AppView/AppControls/UpgradeButton/UpgradeButton.test.tsx b/dashboard/src/components/AppView/AppControls/UpgradeButton/UpgradeButton.test.tsx index 4c9d379ff19..753cd074e18 100644 --- a/dashboard/src/components/AppView/AppControls/UpgradeButton/UpgradeButton.test.tsx +++ b/dashboard/src/components/AppView/AppControls/UpgradeButton/UpgradeButton.test.tsx @@ -23,7 +23,7 @@ const defaultProps = { }; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.installedpackages = { ...actions.installedpackages, @@ -34,7 +34,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx b/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx index ffb3a9e2081..cdce521e6f1 100644 --- a/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx +++ b/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx @@ -14,7 +14,7 @@ import { IAppViewResourceRefs } from "../AppView"; const defaultState = { config: { remoteComponentsUrl: "" }, -}; +} as IStoreState; const defaultProps = { app: { diff --git a/dashboard/src/components/Catalog/Catalog.test.tsx b/dashboard/src/components/Catalog/Catalog.test.tsx index efcd301284b..1e945648732 100644 --- a/dashboard/src/components/Catalog/Catalog.test.tsx +++ b/dashboard/src/components/Catalog/Catalog.test.tsx @@ -167,7 +167,7 @@ it("retrieves csvs in the namespace if operators enabled", () => { const getCSVs = jest.fn(); actions.operators.getCSVs = getCSVs; const state = deepClone(populatedState) as IStoreState; - state.config.featureFlags = { operators: true }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: true }; mountWrapper( getStore(state), @@ -185,7 +185,7 @@ it("not retrieveing csvs in the namespace if operators deactivated", () => { const getCSVs = jest.fn(); actions.operators.getCSVs = getCSVs; const state = deepClone(populatedState) as IStoreState; - state.config.featureFlags = { operators: false }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: false }; mountWrapper( getStore(state), diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoButton.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoButton.test.tsx index 538933549ce..c599915d7c1 100644 --- a/dashboard/src/components/Config/PkgRepoList/PkgRepoButton.test.tsx +++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoButton.test.tsx @@ -21,7 +21,7 @@ jest.mock("./PkgRepoForm", () => { }); let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.repos = { ...actions.repos, @@ -32,7 +32,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoControl.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoControl.test.tsx index 69f6fac006a..d3efff2f681 100644 --- a/dashboard/src/components/Config/PkgRepoList/PkgRepoControl.test.tsx +++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoControl.test.tsx @@ -12,7 +12,7 @@ import { PkgRepoAddButton } from "./PkgRepoButton"; import { IPkgRepoListItemProps, PkgRepoControl } from "./PkgRepoControl"; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.repos = { ...actions.repos, @@ -23,7 +23,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx index 9af85ae18a8..42b229cea40 100644 --- a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx +++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx @@ -89,7 +89,7 @@ const pkgRepoFormData = { } as IPkgRepoFormData; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.repos = { ...actions.repos, @@ -99,7 +99,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx index da6b9fed904..38bb73c58e6 100644 --- a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx +++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx @@ -28,7 +28,7 @@ const { const namespace = clusters[currentCluster].currentNamespace; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.repos = { ...actions.repos, @@ -43,7 +43,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/CustomFormParam.test.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/CustomFormParam.test.tsx index 11f97716f1f..7c2da2f1449 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/CustomFormParam.test.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/CustomFormParam.test.tsx @@ -30,7 +30,7 @@ const defaultProps = { const defaultState = { config: { remoteComponentsUrl: "" }, -}; +} as IStoreState; // Ensure remote-component doesn't trigger external requests during this test. const mockOpen = jest.fn(); diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.test.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.test.tsx index 7e85f831ed0..01c07c054a5 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.test.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.test.tsx @@ -1,6 +1,7 @@ // Copyright 2019-2022 the Kubeapps contributors. // SPDX-License-Identifier: Apache-2.0 +import { CdsButton } from "@cds/react/button"; import { JSONSchemaType } from "ajv"; import { AvailablePackageDetail, @@ -8,7 +9,7 @@ import { } from "gen/kubeappsapis/core/packages/v1alpha1/packages"; import { act } from "react-dom/test-utils"; import { MonacoDiffEditor } from "react-monaco-editor"; -import { defaultStore, mountWrapper } from "shared/specs/mountWrapper"; +import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper"; import { IPackageState } from "shared/types"; import BasicDeploymentForm from "./BasicDeploymentForm"; import DeploymentFormBody, { IDeploymentFormBodyProps } from "./DeploymentFormBody"; @@ -111,31 +112,32 @@ const defaultProps: IDeploymentFormBodyProps = { jest.useFakeTimers(); +const defaultSchema = { + properties: { a: { type: "string" } }, +} as unknown as JSONSchemaType; + +const defaultValues = `a: b + + +c: d +`; + const versions = [{ appVersion: "10.0.0", pkgVersion: "1.2.3" }] as PackageAppVersion[]; +const selected = { + values: defaultValues, + schema: defaultSchema, + versions: [versions[0], { ...versions[0], pkgVersion: "1.2.4" } as PackageAppVersion], + availablePackageDetail: { name: "my-version" } as AvailablePackageDetail, +} as IPackageState["selected"]; // Note that most of the tests that cover DeploymentFormBody component are in // in the DeploymentForm and UpgradeForm parent components // Context at https://github.com/vmware-tanzu/kubeapps/issues/1293 it("should modify the original values of the differential component if parsed as YAML object", () => { - const oldValues = `a: b - - -c: d -`; - const schema = { - properties: { a: { type: "string" } }, - } as unknown as JSONSchemaType; - const selected = { - values: oldValues, - schema, - versions: [versions[0], { ...versions[0], pkgVersion: "1.2.4" } as PackageAppVersion], - availablePackageDetail: { name: "my-version" } as AvailablePackageDetail, - } as IPackageState["selected"]; - const wrapper = mountWrapper( defaultStore, - , + , ); expect( @@ -143,7 +145,7 @@ c: d .find(MonacoDiffEditor) .filterWhere(p => p.prop("language") === "yaml") .prop("original"), - ).toBe(oldValues); + ).toBe(defaultValues); // Trigger a change in the basic form and a YAML parse const input = wrapper @@ -169,3 +171,108 @@ c: d .prop("original"), ).toBe(expectedValues); }); + +it("should not render a schema editor if the feature flag is disabled", () => { + const state = { + ...initialState, + config: { + ...initialState.config, + featureFlags: { ...initialState.config.featureFlags, schemaEditor: false }, + }, + }; + const wrapper = mountWrapper( + getStore(state), + , + ); + + expect( + wrapper.find(MonacoDiffEditor).filterWhere(p => p.prop("language") === "json"), + ).not.toExist(); +}); + +it("should render a schema editor if the feature flag is enabled", () => { + const state = { + ...initialState, + config: { + ...initialState.config, + featureFlags: { ...initialState.config.featureFlags, schemaEditor: true }, + }, + }; + const wrapper = mountWrapper( + getStore(state), + , + ); + + const expectedSchema = `{ + "properties": { + "a": { + "type": "string" + } + } +}`; + + // find the schema editor + expect( + wrapper + .find(MonacoDiffEditor) + .filterWhere(p => p.prop("language") === "json") + .prop("original"), + ).toBe(expectedSchema); + + // ensure the schema is being rendered as a basic form + expect( + wrapper + .find(BasicDeploymentForm) + .find("input") + .filterWhere(i => i.prop("id") === "a"), // the input for the property "a" + ).toExist(); + + // ensure there is no button to update the schema if not modified + expect( + wrapper.find(CdsButton).filterWhere(b => b.text().includes("Update schema")), + ).not.toExist(); + + const newSchema = `{ + "properties": { + "changedPropertyName": { + "type": "string" + } + } + }`; + + // update the schema + act(() => { + ( + wrapper + .find(MonacoDiffEditor) + .filterWhere(p => p.prop("language") === "json") + .prop("onChange") as any + )(newSchema); + }); + wrapper.update(); + + // ensure the new schema is in the editor + expect( + wrapper + .find(MonacoDiffEditor) + .filterWhere(p => p.prop("language") === "json") + .prop("original"), + ).toBe(newSchema); + + // click on the button to update the basic form + act(() => { + wrapper + .find(CdsButton) + .filterWhere(b => b.text().includes("Update schema")) + .simulate("click"); + }); + wrapper.update(); + + // ensure the basic form has been updated + expect( + wrapper + .find(BasicDeploymentForm) + .find("input") + .filterWhere(i => i.prop("id") === "changedPropertyName"), + ).toExist(); +}); diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.tsx index b6ebd893fb8..b6811896948 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/DeploymentFormBody.tsx @@ -11,13 +11,14 @@ import LoadingWrapper from "components/LoadingWrapper"; import Tabs from "components/Tabs"; import { isEmpty } from "lodash"; import { FormEvent, RefObject, useCallback, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; import { retrieveBasicFormParams, schemaToObject, schemaToString, updateCurrentConfigByKey, } from "shared/schema"; -import { DeploymentEvent, IBasicFormParam, IPackageState } from "shared/types"; +import { DeploymentEvent, IBasicFormParam, IPackageState, IStoreState } from "shared/types"; import { getValueFromEvent } from "shared/utils"; import { parseToYamlNode, setPathValueInYamlNode, toStringYamlNode } from "shared/yamlUtils"; import YAML from "yaml"; @@ -58,6 +59,9 @@ function DeploymentFormBody({ pkgVersion, error, } = selected; + const { + config: { featureFlags }, + } = useSelector((state: IStoreState) => state); // Component state const [paramsFromComponentState, setParamsFromComponentState] = useState([] as IBasicFormParam[]); @@ -325,20 +329,22 @@ function DeploymentFormBody({ >, ); - // Text editor creation - tabColumns.push( -
- Schema editor (advanced) -
, - ); - tabData.push( - , - ); + if (featureFlags.schemaEditor) { + // Schema editor creation, if the feature flag is enabled + tabColumns.push( +
+ Schema editor (advanced) +
, + ); + tabData.push( + , + ); + } return (
diff --git a/dashboard/src/components/Header/ContextSelector.test.tsx b/dashboard/src/components/Header/ContextSelector.test.tsx index f28207c3613..b968313bea9 100644 --- a/dashboard/src/components/Header/ContextSelector.test.tsx +++ b/dashboard/src/components/Header/ContextSelector.test.tsx @@ -18,7 +18,7 @@ import ContextSelector from "./ContextSelector"; let spyOnUseDispatch: jest.SpyInstance; let spyOnUseHistory: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.namespace = { ...actions.namespace, @@ -35,7 +35,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); spyOnUseHistory.mockRestore(); }); diff --git a/dashboard/src/components/Header/Menu.test.tsx b/dashboard/src/components/Header/Menu.test.tsx index bf311b9a3fb..982defe2969 100644 --- a/dashboard/src/components/Header/Menu.test.tsx +++ b/dashboard/src/components/Header/Menu.test.tsx @@ -43,7 +43,7 @@ afterEach(() => { it("opens the dropdown full menu", () => { const state = deepClone(initialState) as IStoreState; - state.config.featureFlags = { operators: true }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: true }; const store = getStore(state); const wrapper = mountWrapper(store, ); expect(wrapper.find(".dropdown")).not.toHaveClassName("open"); @@ -61,7 +61,7 @@ it("opens the dropdown full menu", () => { it("opens the dropdown menu without operators item", () => { const state = deepClone(initialState) as IStoreState; - state.config.featureFlags = { operators: false }; + state.config.featureFlags = { ...initialState.config.featureFlags, operators: false }; const store = getStore(state); const wrapper = mountWrapper(store, ); expect(wrapper.find(".dropdown")).not.toHaveClassName("open"); diff --git a/dashboard/src/components/Layout/Layout.test.tsx b/dashboard/src/components/Layout/Layout.test.tsx index c2acbbddb9e..a287fb00d57 100644 --- a/dashboard/src/components/Layout/Layout.test.tsx +++ b/dashboard/src/components/Layout/Layout.test.tsx @@ -3,11 +3,12 @@ import actions from "actions"; import * as ReactRedux from "react-redux"; -import { getStore, mountWrapper } from "shared/specs/mountWrapper"; +import { getStore, initialState, mountWrapper } from "shared/specs/mountWrapper"; +import { IStoreState } from "shared/types"; import Layout from "./Layout"; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.namespace }; +const kubeActions = { ...actions.namespace }; beforeEach(() => { actions.kube = { ...actions.kube, @@ -18,23 +19,27 @@ beforeEach(() => { }); afterEach(() => { - actions.namespace = { ...kubeaActions }; + actions.namespace = { ...kubeActions }; spyOnUseDispatch.mockRestore(); jest.restoreAllMocks(); }); const defaultState = { + ...initialState, clusters: { + ...initialState.clusters, currentCluster: "default", clusters: { + ...initialState.clusters.clusters, default: { + ...initialState.clusters.clusters[initialState.clusters.currentCluster], currentNamespace: "default", namespaces: ["default", "other"], }, }, }, - auth: { authenticated: true }, -}; + auth: { ...initialState.auth, authenticated: true }, +} as IStoreState; it("fetches resource kinds when operators enabled", () => { const state = { @@ -51,7 +56,7 @@ it("fetches resource kinds when operators enabled", () => { expect(actions.kube.getResourceKinds).toHaveBeenCalled(); }); -it("does not fetch resource kinds when operators disaabled", () => { +it("does not fetch resource kinds when operators disabled", () => { mountWrapper(getStore(defaultState), ); expect(actions.kube.getResourceKinds).not.toHaveBeenCalled(); diff --git a/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx b/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx index 70768f2100b..93779f0510a 100644 --- a/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx +++ b/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx @@ -52,7 +52,7 @@ const resource = { } as any; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.operators = { ...actions.operators, @@ -100,7 +100,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); jest.restoreAllMocks(); }); diff --git a/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx b/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx index fb06216196d..7045feaaf4b 100644 --- a/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx +++ b/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx @@ -39,7 +39,7 @@ const defaultCSV = { } as any; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { // mock the window.matchMedia for selecting the theme Object.defineProperty(window, "matchMedia", { @@ -86,7 +86,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); jest.restoreAllMocks(); }); diff --git a/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx b/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx index 392bc3bba3d..730d49bd56b 100644 --- a/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx +++ b/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx @@ -48,7 +48,7 @@ const defaultCSV = { } as any; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.operators = { ...actions.operators, @@ -95,7 +95,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); jest.restoreAllMocks(); }); diff --git a/dashboard/src/components/OperatorList/OperatorList.test.tsx b/dashboard/src/components/OperatorList/OperatorList.test.tsx index 7388f662996..304bfed5422 100644 --- a/dashboard/src/components/OperatorList/OperatorList.test.tsx +++ b/dashboard/src/components/OperatorList/OperatorList.test.tsx @@ -16,7 +16,7 @@ import OperatorItems from "./OperatorItems"; import OperatorList, { filterNames, IOperatorListProps } from "./OperatorList"; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.operators = { ...actions.operators, @@ -29,7 +29,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/OperatorNew/OperatorNew.test.tsx b/dashboard/src/components/OperatorNew/OperatorNew.test.tsx index 4faa220e31d..11a89316ef8 100644 --- a/dashboard/src/components/OperatorNew/OperatorNew.test.tsx +++ b/dashboard/src/components/OperatorNew/OperatorNew.test.tsx @@ -52,7 +52,7 @@ const defaultOperator = { } as any; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.operators = { ...actions.operators, @@ -63,7 +63,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/OperatorView/OperatorView.test.tsx b/dashboard/src/components/OperatorView/OperatorView.test.tsx index 0c26b20ca24..8adc89687a4 100644 --- a/dashboard/src/components/OperatorView/OperatorView.test.tsx +++ b/dashboard/src/components/OperatorView/OperatorView.test.tsx @@ -48,7 +48,7 @@ const defaultOperator = { } as any; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.operators = { ...actions.operators, @@ -60,7 +60,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/PackageHeader/PackageReadme.test.tsx b/dashboard/src/components/PackageHeader/PackageReadme.test.tsx index f93ab18e961..a58d364a86a 100644 --- a/dashboard/src/components/PackageHeader/PackageReadme.test.tsx +++ b/dashboard/src/components/PackageHeader/PackageReadme.test.tsx @@ -13,7 +13,7 @@ const defaultProps = { readme: "", }; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.availablepackages = { ...actions.availablepackages, @@ -21,7 +21,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; }); it("behaves as a loading component", () => { diff --git a/dashboard/src/components/PackageHeader/PackageView.test.tsx b/dashboard/src/components/PackageHeader/PackageView.test.tsx index 360ab989ed6..ba0a8a90283 100644 --- a/dashboard/src/components/PackageHeader/PackageView.test.tsx +++ b/dashboard/src/components/PackageHeader/PackageView.test.tsx @@ -93,7 +93,7 @@ const defaultState = { } as IStoreState; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.kube }; +const kubeActions = { ...actions.kube }; beforeEach(() => { actions.availablepackages = { @@ -107,7 +107,7 @@ beforeEach(() => { }); afterEach(() => { - actions.kube = { ...kubeaActions }; + actions.kube = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx b/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx index 9e9510aee5a..792d47fe9a1 100644 --- a/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx +++ b/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx @@ -27,7 +27,7 @@ const installedPackageDetail = { } as InstalledPackageDetail; let spyOnUseDispatch: jest.SpyInstance; -const kubeaActions = { ...actions.operators }; +const kubeActions = { ...actions.operators }; beforeEach(() => { actions.repos = { ...actions.repos, @@ -38,7 +38,7 @@ beforeEach(() => { }); afterEach(() => { - actions.operators = { ...kubeaActions }; + actions.operators = { ...kubeActions }; spyOnUseDispatch.mockRestore(); }); diff --git a/dashboard/src/containers/LoginFormContainer/LoginFormContainer.test.tsx b/dashboard/src/containers/LoginFormContainer/LoginFormContainer.test.tsx index a49c9b68e13..711bec9488c 100644 --- a/dashboard/src/containers/LoginFormContainer/LoginFormContainer.test.tsx +++ b/dashboard/src/containers/LoginFormContainer/LoginFormContainer.test.tsx @@ -9,6 +9,7 @@ import { IConfigState } from "reducers/config"; import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import { SupportedThemes } from "shared/Config"; +import { initialState } from "shared/specs/mountWrapper"; import { IStoreState } from "shared/types"; import LoginForm from "./LoginFormContainer"; @@ -40,7 +41,7 @@ const makeStore = ( carvelGlobalNamespace: "", appVersion: "", oauthLogoutURI: "", - featureFlags: { operators: false }, + featureFlags: { ...initialState.config.featureFlags }, clusters: [], authProxySkipLoginPage: false, theme: SupportedThemes.light, diff --git a/dashboard/src/containers/RoutesContainer/Routes.test.tsx b/dashboard/src/containers/RoutesContainer/Routes.test.tsx index c6f3bbcfd3c..d247e25f986 100644 --- a/dashboard/src/containers/RoutesContainer/Routes.test.tsx +++ b/dashboard/src/containers/RoutesContainer/Routes.test.tsx @@ -33,6 +33,7 @@ const emptyRouteComponentProps: RouteComponentProps<{}> = { const defaultFeatureFlags: IFeatureFlags = { operators: false, + schemaEditor: false, }; it("invalid path should show a 404 error", () => { diff --git a/dashboard/src/reducers/cluster.test.ts b/dashboard/src/reducers/cluster.test.ts index 1595874a289..e7ab79c2b3a 100644 --- a/dashboard/src/reducers/cluster.test.ts +++ b/dashboard/src/reducers/cluster.test.ts @@ -446,7 +446,7 @@ describe("clusterReducer", () => { oauthLogoutURI: "", featureFlags: { operators: false, - ui: "hex", + schemaEditor: false, }, clusters: ["additionalCluster1", "additionalCluster2"], authProxySkipLoginPage: false, diff --git a/dashboard/src/reducers/config.ts b/dashboard/src/reducers/config.ts index d428cbe1081..178158df385 100644 --- a/dashboard/src/reducers/config.ts +++ b/dashboard/src/reducers/config.ts @@ -22,7 +22,7 @@ export const initialState: IConfigState = { oauthLogoutURI: "", authProxySkipLoginPage: false, clusters: [], - featureFlags: { operators: false }, + featureFlags: { operators: false, schemaEditor: false }, theme: SupportedThemes.light, remoteComponentsUrl: "", customAppViews: [], diff --git a/dashboard/src/shared/Auth.test.ts b/dashboard/src/shared/Auth.test.ts index 6e1c188f543..4fa5a420899 100644 --- a/dashboard/src/shared/Auth.test.ts +++ b/dashboard/src/shared/Auth.test.ts @@ -8,6 +8,7 @@ import * as jwt from "jsonwebtoken"; import { Auth } from "./Auth"; import { SupportedThemes } from "./Config"; import { KubeappsGrpcClient } from "./KubeappsGrpcClient"; +import { initialState } from "./specs/mountWrapper"; describe("Auth", () => { // Create a real client, but we'll stub out the function we're interested in. @@ -236,7 +237,7 @@ describe("Auth", () => { carvelGlobalNamespace: "kapp-controller-packaging-global", appVersion: "2", clusters: [], - featureFlags: { operators: false }, + featureFlags: { ...initialState.config.featureFlags }, authProxySkipLoginPage: false, theme: SupportedThemes.light, remoteComponentsUrl: "", @@ -261,7 +262,7 @@ describe("Auth", () => { carvelGlobalNamespace: "kapp-controller-packaging-global", appVersion: "2", clusters: [], - featureFlags: { operators: false }, + featureFlags: { ...initialState.config.featureFlags }, authProxySkipLoginPage: false, theme: SupportedThemes.light, remoteComponentsUrl: "", diff --git a/dashboard/src/shared/Config.ts b/dashboard/src/shared/Config.ts index ac55389a3e7..f0ed9cf5bec 100644 --- a/dashboard/src/shared/Config.ts +++ b/dashboard/src/shared/Config.ts @@ -42,6 +42,7 @@ export interface IConfig { export interface IFeatureFlags { operators: boolean; + schemaEditor: boolean; } export default class Config { @@ -57,7 +58,7 @@ export default class Config { // getTheme retrieves the different theme preferences and calculates which one is chosen public static getTheme(config: IConfig): SupportedThemes { - // Define a ballback theme in case of errors + // Define a fallback theme in case of errors const fallbackTheme = SupportedThemes.light; // Retrieve the system theme preference (configurable via Values.dashboard.defaultTheme)