diff --git a/dashboard/src/actions/apps.ts b/dashboard/src/actions/apps.ts index 337738bbf26..e79f4f0a315 100644 --- a/dashboard/src/actions/apps.ts +++ b/dashboard/src/actions/apps.ts @@ -189,7 +189,6 @@ export function installPackage( { cluster: targetCluster, namespace: targetNamespace } as Context, releaseName, availablePackageDetail.availablePackageRef, - // TODO(agamez): check if this VersionReference we're using is what we expect { version: availablePackageDetail.version.pkgVersion } as VersionReference, values, ); @@ -233,7 +232,6 @@ export function updateInstalledPackage( if (availablePackageDetail?.version?.pkgVersion) { await App.UpdateInstalledPackage( installedPackageRef, - // TODO(agamez): check if this VersionReference we're using is what we expect { version: availablePackageDetail.version.pkgVersion } as VersionReference, values, ); diff --git a/dashboard/src/components/AppList/AppList.test.tsx b/dashboard/src/components/AppList/AppList.test.tsx index 8a2a4292467..6f13c39e54b 100644 --- a/dashboard/src/components/AppList/AppList.test.tsx +++ b/dashboard/src/components/AppList/AppList.test.tsx @@ -229,7 +229,7 @@ context("when apps available", () => { const wrapper = mountWrapper(getStore(state), ); const itemList = wrapper.find(AppListItem); expect(itemList).toExist(); - expect(itemList.key()).toBe("bar/foo"); + expect(itemList.key()).toBe("foobar-bar/foo"); }); it("filters apps", () => { @@ -239,7 +239,7 @@ context("when apps available", () => { installedPackageRef: { identifier: "foo/bar", pkgVersion: "1.0.0", - context: { cluster: "", namespace: "foobar" } as Context, + context: { cluster: "", namespace: "fooNs" } as Context, plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, } as InstalledPackageReference, status: { @@ -257,7 +257,76 @@ context("when apps available", () => { installedPackageRef: { identifier: "foobar/bar", pkgVersion: "1.0.0", - context: { cluster: "", namespace: "foobar" } as Context, + context: { cluster: "", namespace: "fooNs" } as Context, + plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, + } as InstalledPackageReference, + status: { + ready: true, + reason: InstalledPackageStatus_StatusReason.STATUS_REASON_INSTALLED, + userReason: "deployed", + } as InstalledPackageStatus, + latestMatchingVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + latestVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + currentVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + pkgVersionReference: { version: "1" } as VersionReference, + } as InstalledPackageSummary, + ]; + jest.spyOn(qs, "parse").mockReturnValue({ + q: "bar", + }); + const wrapper = mountWrapper( + getStore(state), + + + , + ); + expect(wrapper.find(AppListItem).key()).toBe("fooNs-foobar/bar"); + }); + + it("filters apps (same name, different ns)", () => { + state.apps.listOverview = [ + { + name: "foo", + installedPackageRef: { + identifier: "foo/bar", + pkgVersion: "1.0.0", + context: { cluster: "", namespace: "fooNs" } as Context, + plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, + } as InstalledPackageReference, + status: { + ready: true, + reason: InstalledPackageStatus_StatusReason.STATUS_REASON_INSTALLED, + userReason: "deployed", + } as InstalledPackageStatus, + latestMatchingVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + latestVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + currentVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + pkgVersionReference: { version: "1" } as VersionReference, + } as InstalledPackageSummary, + { + name: "bar", + installedPackageRef: { + identifier: "foobar/bar", + pkgVersion: "1.0.0", + context: { cluster: "", namespace: "fooNs" } as Context, + plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, + } as InstalledPackageReference, + status: { + ready: true, + reason: InstalledPackageStatus_StatusReason.STATUS_REASON_INSTALLED, + userReason: "deployed", + } as InstalledPackageStatus, + latestMatchingVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + latestVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + currentVersion: { appVersion: "0.1.0", pkgVersion: "1.0.0" } as PackageAppVersion, + pkgVersionReference: { version: "1" } as VersionReference, + } as InstalledPackageSummary, + { + name: "bar", + installedPackageRef: { + identifier: "foobar/bar", + pkgVersion: "1.0.0", + context: { cluster: "", namespace: "barNs" } as Context, plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, } as InstalledPackageReference, status: { @@ -280,7 +349,8 @@ context("when apps available", () => { , ); - expect(wrapper.find(AppListItem).key()).toBe("foobar/bar"); + expect(wrapper.find(AppListItem).first().key()).toBe("fooNs-foobar/bar"); + expect(wrapper.find(AppListItem).last().key()).toBe("barNs-foobar/bar"); }); }); diff --git a/dashboard/src/components/AppList/AppListGrid.tsx b/dashboard/src/components/AppList/AppListGrid.tsx index 9d200a7037d..54d4ce54be1 100644 --- a/dashboard/src/components/AppList/AppListGrid.tsx +++ b/dashboard/src/components/AppList/AppListGrid.tsx @@ -52,7 +52,13 @@ function AppListGrid(props: IAppListProps) { <> {filteredReleases.map(r => { - return ; + return ( + + ); })} {filteredCRs.map(r => { const csv = props.csvs.find(c => diff --git a/dashboard/src/components/AppList/AppListItem.tsx b/dashboard/src/components/AppList/AppListItem.tsx index b76b9b03f04..a55d4989d2e 100644 --- a/dashboard/src/components/AppList/AppListItem.tsx +++ b/dashboard/src/components/AppList/AppListItem.tsx @@ -55,10 +55,10 @@ function AppListItem(props: IAppListItemProps) { <> ); - return ( + return app?.installedPackageRef ? ( + ) : ( + <> ); } diff --git a/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx b/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx index 319444a2fdc..f0a64094024 100644 --- a/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx +++ b/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx @@ -1,10 +1,10 @@ +import actions from "actions"; import Alert from "components/js/Alert"; import LoadingWrapper from "components/LoadingWrapper"; import { AvailablePackageDetail, AvailablePackageReference, Context, - InstalledPackageDetail, InstalledPackageReference, InstalledPackageStatus, InstalledPackageStatus_StatusReason, @@ -67,14 +67,14 @@ const installedPackage1 = { } as InstalledPackageStatus, } as CustomInstalledPackageDetail; -const installedPackageDetail = { +const availablePackageDetail = { availablePackageRef: { context: { cluster: "default", namespace: "my-ns" }, identifier: "test", plugin: { name: PluginNames.PACKAGES_HELM, version: "0.0.1" } as Plugin, }, - currentVersion: { appVersion: "4.5.6", pkgVersion: "1.2.3" }, -} as InstalledPackageDetail; + version: { appVersion: "4.5.6", pkgVersion: "1.2.3" }, +} as AvailablePackageDetail; const selectedPackage = { versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" }], @@ -204,8 +204,8 @@ describe("when an error exists", () => { apps: { error: upgradeError, selected: installedPackage1, - selectedDetails: installedPackageDetail, - } as unknown as IAppState, + selectedDetails: availablePackageDetail, + } as IAppState, packages: { selected: selectedPackage } as IPackageState, }; @@ -225,12 +225,18 @@ describe("when an error exists", () => { }); }); -it("renders the upgrade form when the repo is available", () => { +it("renders the upgrade form when the repo is available, clears state and fetches app", () => { + const getApp = jest.fn(); + actions.apps.getApp = getApp; + const resetSelectedAvailablePackageDetail = jest + .spyOn(actions.packages, "resetSelectedAvailablePackageDetail") + .mockImplementation(jest.fn()); + const state = { apps: { selected: installedPackage1, - selectedDetails: installedPackageDetail, - } as unknown as IAppState, + selectedDetails: availablePackageDetail, + } as IAppState, repos: { repo: repo1, repos: [repo1], @@ -252,14 +258,49 @@ it("renders the upgrade form when the repo is available", () => { expect(wrapper.find(UpgradeForm)).toExist(); expect(wrapper.find(Alert)).not.toExist(); expect(wrapper.find(SelectRepoForm)).not.toExist(); + + expect(resetSelectedAvailablePackageDetail).toHaveBeenCalled(); + expect(getApp).toHaveBeenCalledWith({ + context: { cluster: defaultProps.cluster, namespace: defaultProps.namespace }, + identifier: defaultProps.releaseName, + plugin: defaultProps.plugin, + }); +}); + +it("renders the upgrade form with the version property", () => { + const state = { + apps: { + selected: installedPackage1, + selectedDetails: availablePackageDetail, + } as IAppState, + repos: { + repo: repo1, + repos: [repo1], + isFetching: false, + } as IAppRepositoryState, + packages: { selected: selectedPackage } as IPackageState, + }; + const wrapper = mountWrapper( + getStore({ + ...defaultStore, + ...state, + }), + + + , + + , + ); + expect(wrapper.find(UpgradeForm)).toExist(); + expect(wrapper.find(UpgradeForm)).toHaveProp("version", "0.0.1"); }); it("skips the repo selection form if the app contains upgrade info", () => { const state = { apps: { selected: installedPackage1, - selectedDetails: installedPackageDetail, - } as unknown as IAppState, + selectedDetails: availablePackageDetail, + } as IAppState, repos: { repo: repo1, repos: [repo1], diff --git a/dashboard/src/components/AppUpgrade/AppUpgrade.tsx b/dashboard/src/components/AppUpgrade/AppUpgrade.tsx index dbca17a7d2d..2630ec0771d 100644 --- a/dashboard/src/components/AppUpgrade/AppUpgrade.tsx +++ b/dashboard/src/components/AppUpgrade/AppUpgrade.tsx @@ -18,11 +18,12 @@ interface IRouteParams { releaseName: string; pluginName: string; pluginVersion: string; + version?: string; } function AppUpgrade() { const dispatch: ThunkDispatch = useDispatch(); - const { cluster, namespace, releaseName, pluginName, pluginVersion } = + const { cluster, namespace, releaseName, pluginName, pluginVersion, version } = ReactRouter.useParams() as IRouteParams; const { @@ -41,6 +42,7 @@ function AppUpgrade() { // Initial fetch using the params in the URL useEffect(() => { + dispatch(actions.packages.resetSelectedAvailablePackageDetail()); dispatch( actions.apps.getApp({ context: { cluster: cluster, namespace: namespace }, @@ -70,7 +72,7 @@ function AppUpgrade() { if (installedAppAvailablePackageDetail && installedAppInstalledPackageDetail && selectedPackage) { return (
- +
); } diff --git a/dashboard/src/components/AppView/AppControls/StatusAwareButton/StatusAwareButton.tsx b/dashboard/src/components/AppView/AppControls/StatusAwareButton/StatusAwareButton.tsx index 7fef44f77bf..b0a61ad3582 100644 --- a/dashboard/src/components/AppView/AppControls/StatusAwareButton/StatusAwareButton.tsx +++ b/dashboard/src/components/AppView/AppControls/StatusAwareButton/StatusAwareButton.tsx @@ -27,9 +27,8 @@ export default function StatusAwareButton(pro "The application is being deleted.", [InstalledPackageStatus_StatusReason.STATUS_REASON_PENDING]: "The application is pending installation.", - // 7: "The application is pending upgrade.", // TODO(agamez): do we have a standard code for that? - // 8: "The application is pending rollback.", // TODO(agamez): do we have a standard code for that? }; + const tooltip = releaseStatus?.reason ? tooltips[releaseStatus.reason] : undefined; return ( <> diff --git a/dashboard/src/components/AppView/AppView.test.tsx b/dashboard/src/components/AppView/AppView.test.tsx index 343745fd7ca..d258cb5154f 100644 --- a/dashboard/src/components/AppView/AppView.test.tsx +++ b/dashboard/src/components/AppView/AppView.test.tsx @@ -35,9 +35,10 @@ const routeParams = { cluster: "cluster-1", namespace: "default", releaseName: "mr-sunshine", + plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, }; -const routePathParam = `/foo/${routeParams.cluster}/${routeParams.namespace}/${routeParams.releaseName}`; -const routePath = "/foo/:cluster/:namespace/:releaseName"; +const routePathParam = `/c/${routeParams.cluster}/ns/${routeParams.namespace}/apps/${routeParams.plugin.name}/${routeParams.plugin.version}/${routeParams.releaseName}`; +const routePath = "/c/:cluster/ns/:namespace/apps/:pluginName/:pluginVersion/:releaseName"; let spyOnUseDispatch: jest.SpyInstance; const appActions = { ...actions.apps }; const kubeaActions = { ...actions.kube }; diff --git a/dashboard/src/components/AppView/AppView.tsx b/dashboard/src/components/AppView/AppView.tsx index da6a795b953..319aee421c5 100644 --- a/dashboard/src/components/AppView/AppView.tsx +++ b/dashboard/src/components/AppView/AppView.tsx @@ -207,12 +207,12 @@ export default function AppView() { useEffect(() => { if (!app || !app.manifest) { - return; + return () => {}; } if (Object.values(resourceRefs).some(ref => ref.length)) { // Already populated, skip - return; + return () => {}; } let parsedManifest: IResource[] = YAML.parseAllDocuments(app.manifest).map( @@ -231,6 +231,7 @@ export default function AppView() { // Avoid setting refs if the manifest is empty setResourceRefs(parsedRefs); } + return () => {}; }, [app, cluster, kinds, resourceRefs]); useEffect(() => { diff --git a/dashboard/src/components/AppView/PackageInfo/PackageUpdateInfo.tsx b/dashboard/src/components/AppView/PackageInfo/PackageUpdateInfo.tsx index 1ff3369d1bc..431d9b1381d 100644 --- a/dashboard/src/components/AppView/PackageInfo/PackageUpdateInfo.tsx +++ b/dashboard/src/components/AppView/PackageInfo/PackageUpdateInfo.tsx @@ -37,10 +37,17 @@ export default function PackageUpdateInfo({ installedPackageDetail }: IPackageUp ); } // App is up to date - return alertContent ? ( + return alertContent && installedPackageDetail?.installedPackageRef ? ( {alertContent} - Update Now + + Update Now + ) : (
diff --git a/dashboard/src/components/Catalog/Catalog.test.tsx b/dashboard/src/components/Catalog/Catalog.test.tsx index be118293391..b78112dc654 100644 --- a/dashboard/src/components/Catalog/Catalog.test.tsx +++ b/dashboard/src/components/Catalog/Catalog.test.tsx @@ -165,7 +165,7 @@ it("should render a message if there are no elements in the catalog and the fetc const wrapper = mountWrapper( getStore({ ...defaultState, - packages: { hasFinishedFetching: true } as unknown as IStoreState, + packages: { hasFinishedFetching: true } as IPackageState, }), , ); diff --git a/dashboard/src/components/Catalog/Catalog.tsx b/dashboard/src/components/Catalog/Catalog.tsx index f9e9ff07c44..ba46227a603 100644 --- a/dashboard/src/components/Catalog/Catalog.tsx +++ b/dashboard/src/components/Catalog/Catalog.tsx @@ -176,10 +176,11 @@ export default function Catalog() { if (!supportedCluster || namespace === kubeappsNamespace) { // Global namespace or other cluster, show global repos only dispatch(actions.repos.fetchRepos(kubeappsNamespace)); - return; + return () => {}; } // In other case, fetch global and namespace repos dispatch(actions.repos.fetchRepos(namespace, true)); + return () => {}; }, [dispatch, supportedCluster, namespace, kubeappsNamespace]); useEffect(() => { diff --git a/dashboard/src/components/Catalog/CatalogItem.tsx b/dashboard/src/components/Catalog/CatalogItem.tsx index 298f59c7fe3..84786b2e476 100644 --- a/dashboard/src/components/Catalog/CatalogItem.tsx +++ b/dashboard/src/components/Catalog/CatalogItem.tsx @@ -1,27 +1,25 @@ -import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins"; -import { IRepo } from "shared/types"; -import PackageCatalogItem from "./PackageCatalogItem"; +import { AvailablePackageSummary } from "gen/kubeappsapis/core/packages/v1alpha1/packages"; import OperatorCatalogItem from "./OperatorCatalogItem"; +import PackageCatalogItem from "./PackageCatalogItem"; + +// TODO: this file should be refactored after the operators have been integrated in a plugin export interface ICatalogItem { - id: string; name: string; - version: string; - description: string; cluster: string; namespace: string; - icon?: string; } export interface IPackageCatalogItem extends ICatalogItem { - id: string; - repo: IRepo; - plugin: Plugin; + availablePackageSummary: AvailablePackageSummary; } export interface IOperatorCatalogItem extends ICatalogItem { id: string; csv: string; + version: string; + description: string; + icon?: string; } export interface ICatalogItemProps { diff --git a/dashboard/src/components/Catalog/CatalogItems.tsx b/dashboard/src/components/Catalog/CatalogItems.tsx index f0c566a1d9d..38c165e85e6 100644 --- a/dashboard/src/components/Catalog/CatalogItems.tsx +++ b/dashboard/src/components/Catalog/CatalogItems.tsx @@ -1,10 +1,12 @@ import { AvailablePackageSummary } from "gen/kubeappsapis/core/packages/v1alpha1/packages"; -import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins"; import { useMemo } from "react"; import { getIcon } from "shared/Operators"; -import { IClusterServiceVersion, IRepo } from "shared/types"; -import placeholder from "../../placeholder.png"; -import CatalogItem, { ICatalogItemProps } from "./CatalogItem"; +import { IClusterServiceVersion } from "shared/types"; +import CatalogItem, { + ICatalogItemProps, + IOperatorCatalogItem, + IPackageCatalogItem, +} from "./CatalogItem"; export interface ICatalogItemsProps { availablePackageSummaries: AvailablePackageSummary[]; csvs: IClusterServiceVersion[]; @@ -28,25 +30,16 @@ export default function CatalogItems({ () => availablePackageSummaries.map(c => { return { + // TODO: this should be simplified once the operators are also implemented as a plugin type: `${c.availablePackageRef?.plugin?.name}/${c.availablePackageRef?.plugin?.version}`, id: `package/${c.availablePackageRef?.identifier}`, item: { - plugin: c.availablePackageRef?.plugin ?? ({ name: "", version: "" } as Plugin), - id: `package/${c.availablePackageRef?.identifier}/${c.latestVersion?.pkgVersion}`, name: c.displayName, - icon: c.iconUrl ?? placeholder, - version: c.latestVersion?.pkgVersion ?? "", - description: c.shortDescription, - // TODO(agamez): get the repo name once available - // https://github.com/kubeapps/kubeapps/issues/3165#issuecomment-884574732 - repo: { - name: c.availablePackageRef?.identifier.split("/")[0], - namespace: c.availablePackageRef?.context?.namespace, - } as IRepo, cluster, namespace, - }, - }; + availablePackageSummary: c, + } as IPackageCatalogItem, + } as ICatalogItemProps; }), [availablePackageSummaries, cluster, namespace], ); @@ -68,8 +61,8 @@ export default function CatalogItems({ csv: csv.metadata.name, cluster, namespace, - }, - }; + } as IOperatorCatalogItem, + } as ICatalogItemProps; }); } else { return []; diff --git a/dashboard/src/components/Catalog/PackageCatalogItem.tsx b/dashboard/src/components/Catalog/PackageCatalogItem.tsx index 89970159e06..ecb642314b9 100644 --- a/dashboard/src/components/Catalog/PackageCatalogItem.tsx +++ b/dashboard/src/components/Catalog/PackageCatalogItem.tsx @@ -1,38 +1,53 @@ import { useSelector } from "react-redux"; -import { IRepo, IStoreState } from "shared/types"; +import { IStoreState } from "shared/types"; import * as url from "shared/url"; -import { getPluginIcon, trimDescription } from "shared/utils"; +import { getPluginIcon, PluginNames, trimDescription } from "shared/utils"; import placeholder from "../../placeholder.png"; import InfoCard from "../InfoCard/InfoCard"; import { IPackageCatalogItem } from "./CatalogItem"; export default function PackageCatalogItem(props: IPackageCatalogItem) { - const { icon, name, repo, version, description, namespace, id } = props; - const { - config: { kubeappsNamespace }, - } = useSelector((state: IStoreState) => state); - const iconSrc = icon || placeholder; - const cluster = useSelector((state: IStoreState) => state.clusters.currentCluster); - const link = url.app.packages.get( + const { cluster, namespace, availablePackageSummary } = props; + const { config } = useSelector((state: IStoreState) => state); + + // A package is "global" if its cluster and namespace are the ones in which Kubeapps is installed on + const isGlobal = + availablePackageSummary.availablePackageRef?.context?.namespace === config.kubeappsNamespace && + availablePackageSummary.availablePackageRef?.context?.cluster === config.kubeappsCluster; + + // Use the current cluster/namespace in the URL (passed as props here), + // but, if it is global a "global" segement will be included in the generated URL. + const packageViewLink = url.app.packages.get( cluster, namespace, - name, - repo || ({} as IRepo), - kubeappsNamespace, - props.plugin, + availablePackageSummary.availablePackageRef!, + isGlobal, ); - const bgIcon = getPluginIcon(props.plugin); + + // Historically, this tag is used to show the repository a given package is from, + // but each plugin as its own way to describe the repository right now. + let repositoryName; + switch (availablePackageSummary.availablePackageRef?.plugin?.name) { + case PluginNames.PACKAGES_HELM: + repositoryName = availablePackageSummary.availablePackageRef?.identifier.split("/")[0]; + break; + // TODO: consider the fluxv2 plugin + default: + // Fallback to the plugin name + repositoryName = availablePackageSummary.availablePackageRef?.plugin?.name; + break; + } return ( {repo.name}} - bgIcon={bgIcon} + key={availablePackageSummary.availablePackageRef?.identifier} + title={decodeURIComponent(availablePackageSummary.displayName)} + link={packageViewLink} + info={availablePackageSummary?.latestVersion?.pkgVersion || ""} + icon={availablePackageSummary.iconUrl || placeholder} + description={trimDescription(availablePackageSummary.shortDescription)} + tag1Content={{repositoryName}} + bgIcon={getPluginIcon(availablePackageSummary.availablePackageRef?.plugin)} /> ); } diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx index 5cdef96e5ca..e32b8b1270d 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx @@ -47,15 +47,16 @@ function AppRepoList() { if (!namespace) { // All Namespaces dispatch(actions.repos.fetchRepos("")); - return; + return () => {}; } if (!supportedCluster || namespace === kubeappsNamespace) { // Global namespace or other cluster, show global repos only dispatch(actions.repos.fetchRepos(kubeappsNamespace)); - return; + return () => {}; } // In other case, fetch global and namespace repos dispatch(actions.repos.fetchRepos(namespace, true)); + return () => {}; }, [dispatch, supportedCluster, namespace, kubeappsNamespace]); useEffect(() => { diff --git a/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx b/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx index 616c9b2f3a0..848c9d44271 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx @@ -1,6 +1,7 @@ import actions from "actions"; -import PackageHeader from "components/PackageHeader/PackageHeader"; +import { JSONSchemaType } from "ajv"; import Alert from "components/js/Alert"; +import PackageHeader from "components/PackageHeader/PackageHeader"; import { AvailablePackageDetail, AvailablePackageReference, @@ -21,13 +22,24 @@ const defaultProps = { pkgName: "foo", cluster: "default", namespace: "default", - repo: "repo", releaseName: "my-release", + version: "0.0.1", plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, }; -const routePathParam = `/c/${defaultProps.cluster}/ns/${defaultProps.namespace}/apps/new/${defaultProps.repo}/${defaultProps.plugin.name}/${defaultProps.plugin.version}/${defaultProps.pkgName}/versions/`; + +const defaultSelectedPkg = { + versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" } as PackageAppVersion], + availablePackageDetail: { + name: "test", + availablePackageRef: { identifier: "test/test" }, + } as AvailablePackageDetail, + pkgVersion: "1.2.4", + values: "bar: foo", +}; + +const routePathParam = `/c/${defaultProps.cluster}/ns/${defaultProps.namespace}/apps/new/${defaultProps.plugin.name}/${defaultProps.plugin.version}/${defaultProps.pkgName}/versions/${defaultProps.version}`; const routePath = - "/c/:cluster/ns/:namespace/apps/new/:repo/:pluginName/:pluginVersion/:id/versions"; + "/c/:cluster/ns/:namespace/apps/new/:pluginName/:pluginVersion/:packageId/versions/:packageVersion"; const history = createMemoryHistory({ initialEntries: [routePathParam] }); let spyOnUseDispatch: jest.SpyInstance; @@ -63,42 +75,64 @@ it("fetches the available versions", () => { expect(fetchAvailablePackageVersions).toHaveBeenCalledWith( { context: { cluster: defaultProps.cluster, namespace: defaultProps.namespace }, - identifier: `${defaultProps.repo}/${defaultProps.pkgName}`, + identifier: defaultProps.pkgName, plugin: defaultProps.plugin, } as AvailablePackageReference, - undefined, + defaultProps.version, ); }); describe("renders an error", () => { it("renders a custom error if the deployment failed", () => { const wrapper = mountWrapper( - getStore({ apps: { error: new FetchError("wrong format!") } }), - , + getStore({ + packages: { + selected: { ...defaultSelectedPkg }, + }, + apps: { error: new Error("wrong format!") }, + }), + + + + + , ); - expect(wrapper.find(Alert).exists()).toBe(true); - expect(wrapper.find(Alert).html()).toContain("wrong format!"); + expect(wrapper.find(Alert)).toExist(); + expect( + wrapper.find(Alert).findWhere(a => a.html().includes("An error occurred: wrong format!")), + ).toExist(); + expect(wrapper.find(PackageHeader)).toExist(); }); it("renders a fetch error only", () => { const wrapper = mountWrapper( - getStore({ apps: { error: new FetchError("wrong format!") } }), - , + getStore({ + packages: { selected: { ...defaultSelectedPkg, error: new FetchError("not found") } }, + apps: { error: undefined }, + }), + + + + + , ); expect(wrapper.find(Alert)).toExist(); + expect( + wrapper + .find(Alert) + .findWhere(a => a.html().includes("Unable to retrieve the current app: not found")), + ).toExist(); expect(wrapper.find(PackageHeader)).not.toExist(); }); it("forwards the appValues when modified", () => { - const selected = { - versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" } as PackageAppVersion], - availablePackageDetail: { name: "test" } as AvailablePackageDetail, - pkgVersion: "1.2.4", - values: "bar: foo", - }; const wrapper = mountWrapper( - getStore({ packages: { selected: selected } } as IStoreState), - , + getStore({ packages: { selected: defaultSelectedPkg } } as IStoreState), + + + + + , ); const handleValuesChange: (v: string) => void = wrapper @@ -113,29 +147,25 @@ describe("renders an error", () => { }); it("changes values if the version changes and it has not been modified", () => { - const selected = { - versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" } as PackageAppVersion], - availablePackageDetail: { name: "test" } as AvailablePackageDetail, - pkgVersion: "1.2.4", - values: "bar: foo", - }; const wrapper = mountWrapper( - getStore({ packages: { selected: selected } } as IStoreState), - , + getStore({ packages: { selected: defaultSelectedPkg } } as IStoreState), + + + + + , ); expect(wrapper.find(DeploymentFormBody).prop("appValues")).toBe("bar: foo"); }); it("keep values if the version changes", () => { - const selected = { - versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" } as PackageAppVersion], - availablePackageDetail: { name: "test" } as AvailablePackageDetail, - pkgVersion: "1.2.4", - values: "bar: foo", - }; const wrapper = mountWrapper( - getStore({ packages: { selected: selected } } as IStoreState), - , + getStore({ packages: { selected: defaultSelectedPkg } } as IStoreState), + + + + + , ); const handleValuesChange: (v: string) => void = wrapper @@ -152,7 +182,7 @@ describe("renders an error", () => { expect(wrapper.find(DeploymentFormBody).prop("appValues")).toBe("foo: bar"); wrapper.find("select").simulate("change", { target: { value: "1.2.4" } }); - wrapper.setProps({ selected: { ...selected, values: "bar: foo" } }); + wrapper.setProps({ selected: { ...defaultSelectedPkg, values: "bar: foo" } }); wrapper.update(); expect(wrapper.find(DeploymentFormBody).prop("appValues")).toBe("foo: bar"); }); @@ -164,18 +194,13 @@ describe("renders an error", () => { spyOnUseHistory = jest.spyOn(ReactRouter, "useHistory").mockReturnValue({ push } as any); const appValues = "foo: bar"; - const schema = { properties: { foo: { type: "string", form: true } } }; - const availablePackageDetail = { name: "test" }; - const selected = { - versions: [{ appVersion: "10.0.0", pkgVersion: "1.2.3" } as PackageAppVersion], - availablePackageDetail: availablePackageDetail, - pkgVersion: "1.2.4", - values: appValues, - schema: schema, - }; + const schema = { + properties: { foo: { type: "string", form: true } }, + } as unknown as JSONSchemaType; + const selected = { ...defaultSelectedPkg, values: appValues, schema: schema }; const wrapper = mountWrapper( - getStore({ packages: { selected: selected } } as unknown as IStoreState), + getStore({ packages: { selected: selected } } as IStoreState), @@ -212,14 +237,14 @@ describe("renders an error", () => { expect(installPackage).toHaveBeenCalledWith( defaultProps.cluster, defaultProps.namespace, - availablePackageDetail, + defaultSelectedPkg.availablePackageDetail, defaultProps.releaseName, appValues, schema, ); expect(history.location.pathname).toBe( - "/c/default/ns/default/apps/new/repo/my.plugin/0.0.1/foo/versions/", + "/c/default/ns/default/apps/new/my.plugin/0.0.1/foo/versions/0.0.1", ); }); }); diff --git a/dashboard/src/components/DeploymentForm/DeploymentForm.tsx b/dashboard/src/components/DeploymentForm/DeploymentForm.tsx index 049533231cc..3bc83038c51 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentForm.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentForm.tsx @@ -1,9 +1,9 @@ import actions from "actions"; import AvailablePackageDetailExcerpt from "components/Catalog/AvailablePackageDetailExcerpt"; -import PackageHeader from "components/PackageHeader/PackageHeader"; import Alert from "components/js/Alert"; import Column from "components/js/Column"; import Row from "components/js/Row"; +import PackageHeader from "components/PackageHeader/PackageHeader"; import { push } from "connected-react-router"; import { AvailablePackageReference } from "gen/kubeappsapis/core/packages/v1alpha1/packages"; import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins"; @@ -21,74 +21,67 @@ import LoadingWrapper from "../LoadingWrapper/LoadingWrapper"; interface IRouteParams { cluster: string; namespace: string; - repo: string; global: string; - id: string; pluginName: string; pluginVersion: string; - version?: any; + packageId: string; + packageVersion?: string; } export default function DeploymentForm() { const dispatch: ThunkDispatch = useDispatch(); const { - cluster, - namespace, - repo, + cluster: targetCluster, + namespace: targetNamespace, global, - id, + packageId, pluginName, pluginVersion, - version: packageVersion, + packageVersion, } = ReactRouter.useParams() as IRouteParams; const { - apps, config, - packages: { isFetching: packagesIsFetching, selected }, + packages: { isFetching: packagesIsFetching, selected: selectedPackage }, + apps, } = useSelector((state: IStoreState) => state); - const packageId = `${repo}/${id}`; - const packageNamespace = global === "global" ? config.kubeappsNamespace : namespace; - const packageCluster = global === "global" ? config.kubeappsCluster : cluster; - const error = apps.error || selected.error; - const kubeappsNamespace = config.kubeappsNamespace; - const { availablePackageDetail, versions, schema, values, pkgVersion } = selected; + const [isDeploying, setDeploying] = useState(false); const [releaseName, setReleaseName] = useState(""); - const [appValues, setAppValues] = useState(values || ""); + const [appValues, setAppValues] = useState(selectedPackage.values || ""); const [valuesModified, setValuesModified] = useState(false); - const [pluginObj] = useState( - selected.availablePackageDetail?.availablePackageRef?.plugin ?? - ({ name: pluginName, version: pluginVersion } as Plugin), - ); + + const error = apps.error || selectedPackage.error; + + const [pluginObj] = useState({ name: pluginName, version: pluginVersion } as Plugin); + + // Use the cluster/namespace from the URL unless it comes from a "global" repository. + // In that case, use the cluster/namespace from where kubeapps has been installed on + const isGlobal = global === "global"; + const [packageReference] = useState({ + context: { + cluster: isGlobal ? config.kubeappsCluster : targetCluster, + namespace: isGlobal ? config.kubeappsNamespace : targetNamespace, + }, + plugin: pluginObj, + identifier: packageId, + } as AvailablePackageReference); useEffect(() => { + // Get the package details dispatch( - actions.packages.fetchAvailablePackageVersions({ - context: { cluster: packageCluster, namespace: packageNamespace }, - plugin: pluginObj, - identifier: packageId, - } as AvailablePackageReference), + actions.packages.fetchAndSelectAvailablePackageDetail(packageReference, packageVersion), ); - }, [dispatch, packageCluster, packageNamespace, packageId, pluginObj]); + // Populate the rest of packages versions + dispatch(actions.packages.fetchAvailablePackageVersions(packageReference)); + return () => {}; + }, [dispatch, packageReference, packageVersion]); useEffect(() => { if (!valuesModified) { - setAppValues(values || ""); + setAppValues(selectedPackage.values || ""); } - }, [values, valuesModified]); - - useEffect(() => { - dispatch( - actions.packages.fetchAndSelectAvailablePackageDetail( - { - context: { cluster: packageCluster, namespace: packageNamespace }, - plugin: pluginObj, - identifier: packageId, - } as AvailablePackageReference, - packageVersion, - ), - ); - }, [packageCluster, packageNamespace, packageId, packageVersion, dispatch, pluginObj]); + return () => {}; + }, [selectedPackage.values, valuesModified]); const handleValuesChange = (value: string) => { setAppValues(value); @@ -105,23 +98,26 @@ export default function DeploymentForm() { const handleDeploy = async (e: React.FormEvent) => { e.preventDefault(); setDeploying(true); - if (availablePackageDetail) { + if (selectedPackage.availablePackageDetail) { const deployed = await dispatch( + // Installation always happen in the cluster/namespace passed in the URL actions.apps.installPackage( - cluster, - namespace, - availablePackageDetail, + targetCluster, + targetNamespace, + selectedPackage.availablePackageDetail, releaseName, appValues, - schema, + selectedPackage.schema, ), ); setDeploying(false); if (deployed) { dispatch( push( + // Redirect to the installed package, note that the cluster/ns are the ones passed + // in the URL, not the ones from the package. url.app.apps.get({ - context: { cluster: cluster, namespace: namespace }, + context: { cluster: targetCluster, namespace: targetNamespace }, plugin: pluginObj, identifier: releaseName, } as AvailablePackageReference), @@ -135,12 +131,12 @@ export default function DeploymentForm() { dispatch( push( url.app.apps.new( - cluster, - namespace, - availablePackageDetail!, - e.currentTarget.value, - kubeappsNamespace, + targetCluster, + targetNamespace, pluginObj, + packageId, + e.currentTarget.value, + isGlobal, ), ), ); @@ -156,16 +152,16 @@ export default function DeploymentForm() { ); } - if (!availablePackageDetail) { + if (!selectedPackage.availablePackageDetail) { return ; } return (
{isDeploying && (

@@ -175,7 +171,7 @@ export default function DeploymentForm() { - + {error && An error occurred: {error.message}} @@ -200,9 +196,9 @@ export default function DeploymentForm() { { const wrapper = mount(); - expect(wrapper.text()).toContain("testrepo/test"); + expect(wrapper.text()).toContain("testrepo/foo"); }); it("displays the appVersion", () => { diff --git a/dashboard/src/components/PackageHeader/PackageHeader.tsx b/dashboard/src/components/PackageHeader/PackageHeader.tsx index 155ce254da0..a61349594e1 100644 --- a/dashboard/src/components/PackageHeader/PackageHeader.tsx +++ b/dashboard/src/components/PackageHeader/PackageHeader.tsx @@ -28,18 +28,14 @@ export default function PackageHeader({ deployButton, selectedVersion, }: IPackageHeaderProps) { - return ( + return availablePackageDetail?.availablePackageRef?.identifier ? ( + ) : ( + <> ); } diff --git a/dashboard/src/components/PackageHeader/PackageView.test.tsx b/dashboard/src/components/PackageHeader/PackageView.test.tsx index fd39620a2a7..d79581c03a5 100644 --- a/dashboard/src/components/PackageHeader/PackageView.test.tsx +++ b/dashboard/src/components/PackageHeader/PackageView.test.tsx @@ -26,7 +26,6 @@ const defaultProps = { selected: { versions: [] } as IPackageState["selected"], version: undefined, kubeappsNamespace: "kubeapps", - repo: "testrepo", id: "test", plugin: { name: "my.plugin", version: "0.0.1" } as Plugin, }; @@ -102,8 +101,8 @@ afterEach(() => { spyOnUseDispatch.mockRestore(); }); -const routePathParam = `/c/${defaultProps.cluster}/ns/${defaultProps.packageNamespace}/packages/${defaultProps.repo}/${defaultProps.plugin.name}/${defaultProps.plugin.version}/${defaultProps.id}`; -const routePath = "/c/:cluster/ns/:namespace/packages/:repo/:pluginName/:pluginVersion/:id"; +const routePathParam = `/c/${defaultProps.cluster}/ns/${defaultProps.packageNamespace}/packages/${defaultProps.plugin.name}/${defaultProps.plugin.version}/${defaultProps.id}`; +const routePath = "/c/:cluster/ns/:namespace/packages/:pluginName/:pluginVersion/:packageId"; const history = createMemoryHistory({ initialEntries: [routePathParam] }); it("triggers the fetchAvailablePackageVersions when mounting", () => { @@ -119,7 +118,7 @@ it("triggers the fetchAvailablePackageVersions when mounting", () => { ); expect(spy).toHaveBeenCalledWith({ context: { cluster: defaultProps.cluster, namespace: defaultProps.packageNamespace }, - identifier: `${defaultProps.repo}/${defaultProps.id}`, + identifier: defaultProps.id, plugin: defaultProps.plugin, } as AvailablePackageReference); }); @@ -139,7 +138,7 @@ describe("when receiving new props", () => { expect(spy).toHaveBeenCalledWith( { context: { cluster: defaultProps.cluster, namespace: defaultProps.packageNamespace }, - identifier: "testrepo/test", + identifier: defaultProps.id, plugin: defaultProps.plugin, } as AvailablePackageReference, undefined, @@ -267,7 +266,9 @@ describe("AvailablePackageMaintainers githubIDAsNames prop value", () => { const wrapper = mountWrapper( getStore({ ...defaultState, - packages: { selected: { availablePackageDetail: myAvailablePkgDetail } }, + packages: { + selected: { availablePackageDetail: myAvailablePkgDetail, pkgVersion: "0.0.1" }, + }, } as IStoreState), @@ -288,7 +289,7 @@ it("renders the sources links when set", () => { const wrapper = mountWrapper( getStore({ ...defaultState, - packages: { selected: { availablePackageDetail: myAvailablePkgDetail } }, + packages: { selected: { availablePackageDetail: myAvailablePkgDetail, pkgVersion: "0.0.1" } }, } as IStoreState), @@ -319,7 +320,7 @@ describe("renders errors", () => { getStore({ ...defaultState, packages: { ...defaultPackageState, selected: { error: new Error("Boom!") } }, - } as unknown as IStoreState), + } as IStoreState), diff --git a/dashboard/src/components/PackageHeader/PackageView.tsx b/dashboard/src/components/PackageHeader/PackageView.tsx index 9000f186551..f08e042e54d 100644 --- a/dashboard/src/components/PackageHeader/PackageView.tsx +++ b/dashboard/src/components/PackageHeader/PackageView.tsx @@ -23,40 +23,38 @@ import PackageReadme from "./PackageReadme"; interface IRouteParams { cluster: string; namespace: string; - repo: string; global: string; pluginName: string; pluginVersion: string; - id: string; - version?: string; + packageId: string; + packageVersion?: string; } export default function PackageView() { const dispatch: ThunkDispatch = useDispatch(); + const location = ReactRouter.useLocation(); const { - cluster, - namespace, - repo, + cluster: targetCluster, + namespace: targetNamespace, global, + packageId, pluginName, pluginVersion, - id, - version: queryVersion, + packageVersion, } = ReactRouter.useParams() as IRouteParams; const { config, - packages: { isFetching, selected }, + packages: { isFetching, selected: selectedPackage }, } = useSelector((state: IStoreState) => state); - const { availablePackageDetail, versions, pkgVersion, readmeError, error, readme } = selected; - const packageId = `${repo}/${id}`; - const packageNamespace = global === "global" ? config.kubeappsNamespace : namespace; - const packageCluster = global === "global" ? config.kubeappsCluster : cluster; - const kubeappsNamespace = config.kubeappsNamespace; + const [pluginObj] = useState({ name: pluginName, version: pluginVersion } as Plugin); - const location = ReactRouter.useLocation(); + const isGlobal = global === "global"; - const [pluginObj] = useState({ name: pluginName, version: pluginVersion } as Plugin); + // Use the cluster/namespace from the URL unless it comes from a "global" repository. + // In that case, use the cluster/namespace from where kubeapps has been installed on + const packageCluster = isGlobal ? config.kubeappsCluster : targetCluster; + const packageNamespace = isGlobal ? config.kubeappsNamespace : targetNamespace; // Fetch the selected/latest version on the initial load useEffect(() => { @@ -67,11 +65,11 @@ export default function PackageView() { plugin: pluginObj, identifier: packageId, } as AvailablePackageReference, - queryVersion, + packageVersion, ), ); - return; - }, [dispatch, packageId, packageNamespace, packageCluster, queryVersion, pluginObj]); + return () => {}; + }, [dispatch, packageId, packageNamespace, packageCluster, packageVersion, pluginObj]); // Fetch all versions useEffect(() => { @@ -82,6 +80,7 @@ export default function PackageView() { identifier: packageId, } as AvailablePackageReference), ); + return () => {}; }, [dispatch, packageId, packageNamespace, packageCluster, pluginName, pluginVersion]); // Select version handler @@ -92,33 +91,35 @@ export default function PackageView() { dispatch(push(location.pathname.replace(versionRegex, `/versions/${event.target.value}`))); } else { // Otherwise, append the version - dispatch(push(location.pathname.concat(`/versions/${event.target.value}`))); + const trimmedPath = location.pathname.endsWith("/") + ? location.pathname.slice(0, -1) + : location.pathname; + dispatch(push(trimmedPath.concat(`/versions/${event.target.value}`))); } }; - if (error) { - return Unable to fetch package: {error.message}; + if (selectedPackage.error) { + return Unable to fetch package: {selectedPackage.error.message}; } - if (isFetching || !availablePackageDetail) { + if (isFetching || !selectedPackage.availablePackageDetail || !selectedPackage.pkgVersion) { return ; } - return (
@@ -126,26 +127,26 @@ export default function PackageView() { } - selectedVersion={pkgVersion} + selectedVersion={selectedPackage.pkgVersion} />
- + - +
diff --git a/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx b/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx index 5684125262e..c32e8677bf0 100644 --- a/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx +++ b/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx @@ -304,6 +304,49 @@ it("defaults the upgrade version to the current version", () => { expect(wrapper.find(DeploymentFormBody).prop("packageVersion")).toBe("1.0.0"); }); +it("uses the selected version passed in the component's props", () => { + const mockDispatch = jest.fn().mockReturnValue(true); + jest.spyOn(ReactRedux, "useDispatch").mockReturnValue(mockDispatch); + const fetchAndSelectAvailablePackageDetail = jest.fn(); + actions.packages.fetchAndSelectAvailablePackageDetail = fetchAndSelectAvailablePackageDetail; + + const state = { + ...defaultStore, + apps: { + selected: installedPkgDetail, + selectedDetails: availablePkgDetail, + isFetching: false, + } as IAppState, + packages: { + selected: { + ...selectedPkg, + versions: [] as PackageAppVersion[], + }, + } as IPackageState, + }; + + mountWrapper( + getStore({ ...state }), + + + , + + , + ); + + expect(fetchAndSelectAvailablePackageDetail).toHaveBeenCalledWith( + { + context: { + cluster: defaultProps.cluster, + namespace: defaultProps.repoNamespace, + }, + identifier: defaultProps.packageId, + plugin: defaultProps.plugin, + }, + "1.5.0", + ); +}); + it("forwards the appValues when modified", () => { const state = { ...defaultStore, @@ -384,7 +427,7 @@ it("triggers an upgrade when submitting the form", async () => { ); expect(mockDispatch).toHaveBeenCalledWith({ payload: { - args: [url.app.apps.get(installedPkgDetail.installedPackageRef)], + args: [url.app.apps.get(installedPkgDetail.installedPackageRef!)], method: "push", }, type: "@@router/CALL_HISTORY_METHOD", diff --git a/dashboard/src/components/UpgradeForm/UpgradeForm.tsx b/dashboard/src/components/UpgradeForm/UpgradeForm.tsx index acceb05fc86..492336f98ca 100644 --- a/dashboard/src/components/UpgradeForm/UpgradeForm.tsx +++ b/dashboard/src/components/UpgradeForm/UpgradeForm.tsx @@ -7,28 +7,20 @@ import PackageHeader from "components/PackageHeader/PackageHeader"; import PackageVersionSelector from "components/PackageHeader/PackageVersionSelector"; import { push } from "connected-react-router"; import * as jsonpatch from "fast-json-patch"; -import { - AvailablePackageDetail, - InstalledPackageDetail, -} from "gen/kubeappsapis/core/packages/v1alpha1/packages"; import * as yaml from "js-yaml"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Action } from "redux"; import { ThunkDispatch } from "redux-thunk"; import { deleteValue, setValue } from "../../shared/schema"; -import { IPackageState, IStoreState } from "../../shared/types"; +import { IStoreState } from "../../shared/types"; import * as url from "../../shared/url"; import DeploymentFormBody from "../DeploymentFormBody/DeploymentFormBody"; import LoadingWrapper from "../LoadingWrapper/LoadingWrapper"; import "./UpgradeForm.css"; export interface IUpgradeFormProps { - installedAppAvailablePackageDetail: AvailablePackageDetail; - installedAppInstalledPackageDetail: InstalledPackageDetail; - selectedPackage: IPackageState["selected"]; - chartsIsFetching: boolean; - error?: Error; + version?: string; } function applyModifications(mods: jsonpatch.Operation[], values: string) { @@ -47,7 +39,7 @@ function applyModifications(mods: jsonpatch.Operation[], values: string) { return values; } -function UpgradeForm() { +function UpgradeForm(props: IUpgradeFormProps) { const dispatch: ThunkDispatch = useDispatch(); const { @@ -89,12 +81,22 @@ function UpgradeForm() { ), ); } + // If a version has been manually selected (eg. in the URL), fetch it explicitly + if (props.version) { + dispatch( + actions.packages.fetchAndSelectAvailablePackageDetail( + installedAppInstalledPackageDetail?.availablePackageRef, + props.version, + ), + ); + } } }, [ dispatch, installedAppInstalledPackageDetail?.availablePackageRef, selectedPackage.versions.length, installedAppAvailablePackageDetail, + props.version, ]); useEffect(() => { diff --git a/dashboard/src/containers/RoutesContainer/Routes.tsx b/dashboard/src/containers/RoutesContainer/Routes.tsx index 8280e49ed94..d1e402e042f 100644 --- a/dashboard/src/containers/RoutesContainer/Routes.tsx +++ b/dashboard/src/containers/RoutesContainer/Routes.tsx @@ -27,18 +27,19 @@ const privateRoutes = { "/c/:cluster/ns/:namespace/apps": AppList, "/c/:cluster/ns/:namespace/apps/:pluginName/:pluginVersion/:releaseName": AppView, "/c/:cluster/ns/:namespace/apps/:pluginName/:pluginVersion/:releaseName/upgrade": AppUpgrade, - "/c/:cluster/ns/:namespace/apps/new/:repo/:pluginName/:pluginVersion/:id/versions/:version": + "/c/:cluster/ns/:namespace/apps/:pluginName/:pluginVersion/:releaseName/upgrade/:version": + AppUpgrade, + "/c/:cluster/ns/:namespace/apps/new/:pluginName/:pluginVersion/:packageId/versions/:packageVersion": DeploymentForm, - "/c/:cluster/ns/:namespace/apps/new-from-:global(global)/:repo/:pluginName/:pluginVersion/:id/versions/:version": + "/c/:cluster/ns/:namespace/apps/new-from-:global(global)/:pluginName/:pluginVersion/:packageId/versions/:packageVersion": DeploymentForm, "/c/:cluster/ns/:namespace/catalog": Catalog, - "/c/:cluster/ns/:namespace/catalog/:repo": Catalog, - "/c/:cluster/ns/:namespace/packages/:repo/:pluginName/:pluginVersion/:id": PackageView, - "/c/:cluster/ns/:namespace/packages/:repo/:pluginName/:pluginVersion/:id/versions/:version": + "/c/:cluster/ns/:namespace/packages/:pluginName/:pluginVersion/:packageId": PackageView, + "/c/:cluster/ns/:namespace/packages/:pluginName/:pluginVersion/:packageId/versions/:packageVersion": PackageView, - "/c/:cluster/ns/:namespace/:global(global)-packages/:repo/:pluginName/:pluginVersion/:id": + "/c/:cluster/ns/:namespace/:global(global)-packages/:pluginName/:pluginVersion/:packageId": PackageView, - "/c/:cluster/ns/:namespace/:global(global)-packages/:repo/:pluginName/:pluginVersion/:id/versions/:version": + "/c/:cluster/ns/:namespace/:global(global)-packages/:pluginName/:pluginVersion/:packageId/versions/:packageVersion": PackageView, "/c/:cluster/ns/:namespace/operators": OperatorsListContainer, "/c/:cluster/ns/:namespace/operators/:operator": OperatorViewContainer, diff --git a/dashboard/src/reducers/apps.ts b/dashboard/src/reducers/apps.ts index c6b9c9dc3f2..3423b38b130 100644 --- a/dashboard/src/reducers/apps.ts +++ b/dashboard/src/reducers/apps.ts @@ -16,7 +16,12 @@ const appsReducer = ( ): IAppState => { switch (action.type) { case getType(actions.apps.requestApps): - return { ...state, isFetching: true }; + return { + ...state, + isFetching: true, + selected: undefined, + selectedDetails: undefined, + }; case getType(actions.apps.errorApp): return { ...state, isFetching: false, error: action.payload }; case getType(actions.apps.selectApp): diff --git a/dashboard/src/shared/url.ts b/dashboard/src/shared/url.ts index 9b357b99a2a..d04e266d500 100644 --- a/dashboard/src/shared/url.ts +++ b/dashboard/src/shared/url.ts @@ -1,51 +1,55 @@ import { - AvailablePackageDetail, + AvailablePackageReference, InstalledPackageReference, } from "gen/kubeappsapis/core/packages/v1alpha1/packages"; import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins"; -import { IRepo } from "./types"; export const app = { apps: { new: ( cluster: string, namespace: string, - availablePackageDetail: AvailablePackageDetail, - version: string, - globalNamespace: string, plugin: Plugin, + packageId: string, + version: string, + isGlobal: boolean, ) => { - const repoNamespace = availablePackageDetail.availablePackageRef?.context?.namespace; - const newSegment = globalNamespace !== repoNamespace ? "new" : "new-from-global"; - // TODO(agamez): get the repo name once available - // https://github.com/kubeapps/kubeapps/issues/3165#issuecomment-884574732 - const repoName = - availablePackageDetail.availablePackageRef?.identifier.split("/")?.[0] ?? globalNamespace; - return `/c/${cluster}/ns/${namespace}/apps/${newSegment}/${repoName}/${plugin.name}/${ - plugin.version - }/${encodeURIComponent(availablePackageDetail.name)}/versions/${version}`; + const globalSegment = isGlobal ? "new-from-global" : "new"; + return `/c/${cluster}/ns/${namespace}/apps/${globalSegment}/${plugin?.name}/${ + plugin?.version + }/${encodeURIComponent(packageId)}/versions/${version}`; }, list: (cluster?: string, namespace?: string) => `/c/${cluster}/ns/${namespace}/apps`, - get: (ref?: InstalledPackageReference) => - `${app.apps.list(ref?.context?.cluster, ref?.context?.namespace)}/${ref?.plugin?.name}/${ - ref?.plugin?.version - }/${ref?.identifier}`, - upgrade: (ref?: InstalledPackageReference) => `${app.apps.get(ref)}/upgrade`, + get: (installedPackageReference: InstalledPackageReference) => { + const pkgCluster = installedPackageReference?.context?.cluster; + const pkgNamespace = installedPackageReference?.context?.namespace; + const pkgPluginName = installedPackageReference?.plugin?.name; + const pkgPluginVersion = installedPackageReference?.plugin?.version; + const pkgId = installedPackageReference?.identifier || ""; + return `${app.apps.list( + pkgCluster, + pkgNamespace, + )}/${pkgPluginName}/${pkgPluginVersion}/${pkgId}`; + }, + upgrade: (ref: InstalledPackageReference) => `${app.apps.get(ref)}/upgrade`, + upgradeTo: (ref: InstalledPackageReference, version?: string) => + `${app.apps.get(ref)}/upgrade/${version}`, }, catalog: (cluster: string, namespace: string) => `/c/${cluster}/ns/${namespace}/catalog`, packages: { get: ( cluster: string, namespace: string, - packageName: string, - repo: IRepo, - globalNamespace: string, - plugin: Plugin, + availablePackageReference: AvailablePackageReference, + isGlobal: boolean, ) => { - const packagesSegment = globalNamespace === repo.namespace ? "global-packages" : "packages"; - return `/c/${cluster}/ns/${namespace}/${packagesSegment}/${repo.name}/${plugin.name}/${ - plugin.version - }/${encodeURIComponent(packageName)}`; + const pkgPluginName = availablePackageReference?.plugin?.name; + const pkgPluginVersion = availablePackageReference?.plugin?.version; + const pkgId = availablePackageReference?.identifier || ""; + const globalSegment = isGlobal ? "global-packages" : "packages"; + return `/c/${cluster}/ns/${namespace}/${globalSegment}/${pkgPluginName}/${pkgPluginVersion}/${encodeURIComponent( + pkgId, + )}`; }, }, operators: {