From a4d74a556a961ae23ce689b324bbfc048e94ef11 Mon Sep 17 00:00:00 2001 From: Jesse Hu Date: Mon, 22 Jul 2019 17:08:35 +0800 Subject: [PATCH 1/2] Add 'Refresh All App Repositories' button Close #582. Add a button next to 'Add App Repository' to refresh all app repositories. --- dashboard/src/actions/repos.ts | 24 ++++++++++++++ .../Config/AppRepoList/AppRepoButton.test.tsx | 2 +- .../Config/AppRepoList/AppRepoButton.tsx | 4 +-- .../Config/AppRepoList/AppRepoList.tsx | 17 +++++++++- .../AppRepoList/AppRepoRefreshAllButton.tsx | 31 +++++++++++++++++++ .../__snapshots__/AppRepoButton.test.tsx.snap | 6 ++-- .../RepoListContainer/RepoListContainer.ts | 3 ++ dashboard/src/shared/types.ts | 1 + 8 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx diff --git a/dashboard/src/actions/repos.ts b/dashboard/src/actions/repos.ts index d931f3267d8..f0b2336c646 100644 --- a/dashboard/src/actions/repos.ts +++ b/dashboard/src/actions/repos.ts @@ -101,6 +101,30 @@ export const resyncRepo = ( }; }; +export const resyncAllRepos = (): ThunkAction, IStoreState, null, AppReposAction> => { + return async (dispatch, getState) => { + try { + const { + config: { namespace }, + } = getState(); + // TODO: Do something to show progress + const repos = await AppRepository.list(namespace); + if (repos.items.length > 0) { + repos.items.forEach(repo => { + if (repo && repo.spec) { + repo.spec.resyncRequests = repo.spec.resyncRequests || 0; + repo.spec.resyncRequests++; + const name = repo.metadata.name; + AppRepository.update(name, namespace, repo); + } + }); + } + } catch (e) { + dispatch(errorRepos(e, "update")); + } + }; +}; + export const fetchRepos = (): ThunkAction, IStoreState, null, AppReposAction> => { return async (dispatch, getState) => { dispatch(requestRepos()); diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx index dd1cecbe930..0abeeabff97 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx @@ -13,7 +13,7 @@ const defaultProps = { it("should open a modal with the repository form", () => { const wrapper = mount(); - ReactModal.setAppElement(document.createElement("div")); + ReactModal.setAppElement(document.createElement("span")); wrapper.setState({ modalIsOpen: true }); expect(wrapper).toMatchSnapshot(); }); diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx index 2c5143c9821..9f48b1e2282 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx @@ -43,7 +43,7 @@ export class AppRepoAddButton extends React.Component< public render() { const { redirectTo } = this.props; return ( -
+ @@ -64,7 +64,7 @@ export class AppRepoAddButton extends React.Component< {redirectTo && } -
+ ); } diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx index 9123159b513..8b803449b71 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx @@ -4,6 +4,7 @@ import { IAppRepository, IRBACRole } from "../../../shared/types"; import ErrorSelector from "../../ErrorAlert/ErrorSelector"; import { AppRepoAddButton } from "./AppRepoButton"; import { AppRepoListItem } from "./AppRepoListItem"; +import { AppRepoRefreshAllButton } from "./AppRepoRefreshAllButton"; export interface IAppRepoListProps { errors: { @@ -16,6 +17,7 @@ export interface IAppRepoListProps { fetchRepos: () => void; deleteRepo: (name: string) => Promise; resyncRepo: (name: string) => void; + resyncAllRepos: () => void; install: (name: string, url: string, authHeader: string, customCA: string) => Promise; kubeappsNamespace: string; } @@ -61,7 +63,15 @@ class AppRepoList extends React.Component { } public render() { - const { errors, repos, install, deleteRepo, resyncRepo, kubeappsNamespace } = this.props; + const { + errors, + repos, + install, + deleteRepo, + resyncRepo, + resyncAllRepos, + kubeappsNamespace, + } = this.props; return (

App Repositories

@@ -92,6 +102,11 @@ class AppRepoList extends React.Component { install={install} kubeappsNamespace={kubeappsNamespace} /> +
); } diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx new file mode 100644 index 00000000000..fbe49335075 --- /dev/null +++ b/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; + +import "./AppRepo.css"; + +interface IAppRepoRefreshAllButtonProps { + error?: Error; + resyncAllRepos: () => void; + kubeappsNamespace: string; +} + +export class AppRepoRefreshAllButton extends React.Component { + public render() { + return ( + + + + ); + } + + private handleResyncAllClick() { + return () => { + this.props.resyncAllRepos(); + }; + } +} diff --git a/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap b/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap index 5c4c53a1bfc..d543d4f60d8 100644 --- a/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap +++ b/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap @@ -5,9 +5,7 @@ exports[`should open a modal with the repository form 1`] = ` install={[Function]} kubeappsNamespace="kubeapps" > -
+
+
`; diff --git a/dashboard/src/containers/RepoListContainer/RepoListContainer.ts b/dashboard/src/containers/RepoListContainer/RepoListContainer.ts index 6ee6a4bbf3e..8c0ba9f13b3 100644 --- a/dashboard/src/containers/RepoListContainer/RepoListContainer.ts +++ b/dashboard/src/containers/RepoListContainer/RepoListContainer.ts @@ -28,6 +28,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch) resyncRepo: async (name: string) => { return dispatch(actions.repos.resyncRepo(name)); }, + resyncAllRepos: async () => { + return dispatch(actions.repos.resyncAllRepos()); + }, }; } diff --git a/dashboard/src/shared/types.ts b/dashboard/src/shared/types.ts index b0a27401f8f..8b2e0ed0fb0 100644 --- a/dashboard/src/shared/types.ts +++ b/dashboard/src/shared/types.ts @@ -277,6 +277,7 @@ export interface IAppRepository }; }; }; + resyncRequests: number; }, undefined > {} From 24d9577579c9761d52a4dc0f4403b118b758caa6 Mon Sep 17 00:00:00 2001 From: Jesse Hu Date: Thu, 25 Jul 2019 00:36:09 +0800 Subject: [PATCH 2/2] Call dispatch(resyncRepo(name)) to avoid duplicate code --- dashboard/src/actions/repos.test.tsx | 34 -- dashboard/src/actions/repos.ts | 29 +- .../Config/AppRepoList/AppRepoButton.test.tsx | 2 +- .../Config/AppRepoList/AppRepoButton.tsx | 4 +- .../Config/AppRepoList/AppRepoList.tsx | 4 +- .../AppRepoList/AppRepoRefreshAllButton.tsx | 34 +- .../__snapshots__/AppRepoButton.test.tsx.snap | 514 +++++++++--------- .../RepoListContainer/RepoListContainer.ts | 4 +- 8 files changed, 287 insertions(+), 338 deletions(-) diff --git a/dashboard/src/actions/repos.test.tsx b/dashboard/src/actions/repos.test.tsx index b5f75918dab..6adc2ec3287 100644 --- a/dashboard/src/actions/repos.test.tsx +++ b/dashboard/src/actions/repos.test.tsx @@ -112,21 +112,6 @@ describe("deleteRepo", () => { }); describe("resyncRepo", () => { - it("dispatches requestRepos and receiveRepos if no error", async () => { - const expectedActions = [ - { - type: getType(repoActions.requestRepos), - }, - { - type: getType(repoActions.receiveRepos), - payload: { foo: "bar" }, - }, - ]; - - await store.dispatch(repoActions.resyncRepo("foo")); - expect(store.getActions()).toEqual(expectedActions); - }); - it("dispatches errorRepos if error on #get", async () => { AppRepository.get = jest.fn().mockImplementationOnce(() => { throw new Error("Boom!"); @@ -158,25 +143,6 @@ describe("resyncRepo", () => { await store.dispatch(repoActions.resyncRepo("foo")); expect(store.getActions()).toEqual(expectedActions); }); - - it("dispatches requestRepos and errorRepos if error on #list", async () => { - AppRepository.list = jest.fn().mockImplementationOnce(() => { - throw new Error("Boom!"); - }); - - const expectedActions = [ - { - type: getType(repoActions.requestRepos), - }, - { - type: getType(repoActions.errorRepos), - payload: { err: new Error("Boom!"), op: "update" }, - }, - ]; - - await store.dispatch(repoActions.resyncRepo("foo")); - expect(store.getActions()).toEqual(expectedActions); - }); }); describe("fetchRepos", () => { diff --git a/dashboard/src/actions/repos.ts b/dashboard/src/actions/repos.ts index f0b2336c646..0ded8eabc71 100644 --- a/dashboard/src/actions/repos.ts +++ b/dashboard/src/actions/repos.ts @@ -92,36 +92,19 @@ export const resyncRepo = ( repo.spec.resyncRequests++; await AppRepository.update(name, namespace, repo); // TODO: Do something to show progress - dispatch(requestRepos()); - const repos = await AppRepository.list(namespace); - dispatch(receiveRepos(repos.items)); } catch (e) { dispatch(errorRepos(e, "update")); } }; }; -export const resyncAllRepos = (): ThunkAction, IStoreState, null, AppReposAction> => { +export const resyncAllRepos = ( + repoNames: string[], +): ThunkAction, IStoreState, null, AppReposAction> => { return async (dispatch, getState) => { - try { - const { - config: { namespace }, - } = getState(); - // TODO: Do something to show progress - const repos = await AppRepository.list(namespace); - if (repos.items.length > 0) { - repos.items.forEach(repo => { - if (repo && repo.spec) { - repo.spec.resyncRequests = repo.spec.resyncRequests || 0; - repo.spec.resyncRequests++; - const name = repo.metadata.name; - AppRepository.update(name, namespace, repo); - } - }); - } - } catch (e) { - dispatch(errorRepos(e, "update")); - } + repoNames.forEach(name => { + dispatch(resyncRepo(name)); + }); }; }; diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx index 0abeeabff97..dd1cecbe930 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx @@ -13,7 +13,7 @@ const defaultProps = { it("should open a modal with the repository form", () => { const wrapper = mount(); - ReactModal.setAppElement(document.createElement("span")); + ReactModal.setAppElement(document.createElement("div")); wrapper.setState({ modalIsOpen: true }); expect(wrapper).toMatchSnapshot(); }); diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx index 9f48b1e2282..797bf9fdcec 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx @@ -43,7 +43,7 @@ export class AppRepoAddButton extends React.Component< public render() { const { redirectTo } = this.props; return ( - + @@ -64,7 +64,7 @@ export class AppRepoAddButton extends React.Component< {redirectTo && } - + ); } diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx index 8b803449b71..2e0909faf91 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoList.tsx @@ -17,7 +17,7 @@ export interface IAppRepoListProps { fetchRepos: () => void; deleteRepo: (name: string) => Promise; resyncRepo: (name: string) => void; - resyncAllRepos: () => void; + resyncAllRepos: (names: string[]) => void; install: (name: string, url: string, authHeader: string, customCA: string) => Promise; kubeappsNamespace: string; } @@ -103,8 +103,8 @@ class AppRepoList extends React.Component { kubeappsNamespace={kubeappsNamespace} /> diff --git a/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx b/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx index fbe49335075..38c786eefab 100644 --- a/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx +++ b/dashboard/src/components/Config/AppRepoList/AppRepoRefreshAllButton.tsx @@ -1,31 +1,33 @@ import * as React from "react"; +import { IAppRepository } from "shared/types"; import "./AppRepo.css"; interface IAppRepoRefreshAllButtonProps { - error?: Error; - resyncAllRepos: () => void; + resyncAllRepos: (names: string[]) => void; + repos: IAppRepository[]; kubeappsNamespace: string; } export class AppRepoRefreshAllButton extends React.Component { public render() { return ( - - - + ); } - private handleResyncAllClick() { - return () => { - this.props.resyncAllRepos(); - }; - } + private handleResyncAllClick = async () => { + if (this.props.repos) { + const repoNames = this.props.repos.map(repo => { + return repo.metadata.name; + }); + this.props.resyncAllRepos(repoNames); + } + }; } diff --git a/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap b/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap index d543d4f60d8..e21ba15af6d 100644 --- a/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap +++ b/dashboard/src/components/Config/AppRepoList/__snapshots__/AppRepoButton.test.tsx.snap @@ -5,299 +5,297 @@ exports[`should open a modal with the repository form 1`] = ` install={[Function]} kubeappsNamespace="kubeapps" > - - - - + Add App Repository + + +