diff --git a/pkg/app/web/src/components/application-detail.test.tsx b/pkg/app/web/src/components/application-detail.test.tsx index bdce21de8f..5bf4fe0e28 100644 --- a/pkg/app/web/src/components/application-detail.test.tsx +++ b/pkg/app/web/src/components/application-detail.test.tsx @@ -2,10 +2,11 @@ import { DeepPartial } from "@reduxjs/toolkit"; import userEvent from "@testing-library/user-event"; import React from "react"; import { MemoryRouter } from "react-router"; -import { createStore, render, screen } from "../../test-utils"; +import { createStore, render, screen, waitFor } from "../../test-utils"; import { server } from "../mocks/server"; import { AppState } from "../modules"; import { syncApplication } from "../modules/applications"; +import { SyncStrategy } from "../modules/deployments"; import { dummyApplication } from "../__fixtures__/dummy-application"; import { dummyApplicationLiveState } from "../__fixtures__/dummy-application-live-state"; import { dummyEnv } from "../__fixtures__/dummy-environment"; @@ -71,27 +72,68 @@ describe("ApplicationDetail", () => { expect(screen.getByText(dummyApplication.name)).toBeInTheDocument(); expect(screen.getByText("Healthy")).toBeInTheDocument(); expect(screen.getByText("Synced")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /sync/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /sync$/i })).toBeInTheDocument(); }); - it("dispatch sync action if click sync button", async () => { - const store = createStore(baseState); - render( - - - , - { - store, - } - ); + describe("sync", () => { + it("dispatch sync action if click sync button", async () => { + const store = createStore(baseState); + render( + + + , + { + store, + } + ); - userEvent.click(screen.getByRole("button", { name: /sync/i })); + userEvent.click(screen.getByRole("button", { name: /sync$/i })); - expect(store.getActions()).toMatchObject([ - { - type: syncApplication.pending.type, - meta: { arg: { applicationId: dummyApplication.id } }, - }, - ]); + await waitFor(() => + expect(store.getActions()).toMatchObject([ + { + type: syncApplication.pending.type, + meta: { + arg: { + applicationId: dummyApplication.id, + syncStrategy: SyncStrategy.AUTO, + }, + }, + }, + ]) + ); + }); + + it("dispatch sync action with selected sync strategy if changed strategy and click the sync button", async () => { + const store = createStore(baseState); + render( + + + , + { + store, + } + ); + + userEvent.click( + screen.getByRole("button", { name: /select sync strategy/i }) + ); + userEvent.click(screen.getByRole("menuitem", { name: /pipeline sync/i })); + userEvent.click(screen.getByRole("button", { name: /pipeline sync/i })); + + await waitFor(() => + expect(store.getActions()).toMatchObject([ + { + type: syncApplication.pending.type, + meta: { + arg: { + applicationId: dummyApplication.id, + syncStrategy: SyncStrategy.PIPELINE, + }, + }, + }, + ]) + ); + }); }); }); diff --git a/pkg/app/web/src/components/application-detail.tsx b/pkg/app/web/src/components/application-detail.tsx index c71c076907..f67504f863 100644 --- a/pkg/app/web/src/components/application-detail.tsx +++ b/pkg/app/web/src/components/application-detail.tsx @@ -1,7 +1,6 @@ import { Box, Button, - CircularProgress, Link, makeStyles, Paper, @@ -10,14 +9,16 @@ import { import SyncIcon from "@material-ui/icons/Cached"; import OpenInNewIcon from "@material-ui/icons/OpenInNew"; import Skeleton from "@material-ui/lab/Skeleton/Skeleton"; +import { SerializedError } from "@reduxjs/toolkit"; import dayjs from "dayjs"; import React, { FC, memo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Link as RouterLink } from "react-router-dom"; -import { PAGE_PATH_DEPLOYMENTS } from "../constants/path"; import { APPLICATION_KIND_TEXT } from "../constants/application-kind"; import { APPLICATION_SYNC_STATUS_TEXT } from "../constants/application-sync-status-text"; import { APPLICATION_HEALTH_STATUS_TEXT } from "../constants/health-status-text"; +import { PAGE_PATH_DEPLOYMENTS } from "../constants/path"; +import { UI_TEXT_REFRESH } from "../constants/ui-text"; import { AppState } from "../modules"; import { Application, @@ -30,6 +31,7 @@ import { ApplicationLiveState, selectById as selectLiveStateById, } from "../modules/applications-live-state"; +import { SyncStrategy } from "../modules/deployments"; import { Environment, selectById as selectEnvById, @@ -37,10 +39,9 @@ import { import { Piped, selectById as selectPipeById } from "../modules/pipeds"; import { DetailTableRow } from "./detail-table-row"; import { ApplicationHealthStatusIcon } from "./health-status-icon"; +import { SplitButton } from "./split-button"; import { SyncStateReason } from "./sync-state-reason"; import { SyncStatusIcon } from "./sync-status-icon"; -import { SerializedError } from "@reduxjs/toolkit"; -import { UI_TEXT_REFRESH } from "../constants/ui-text"; const useStyles = makeStyles((theme) => ({ root: { @@ -138,6 +139,13 @@ const MostRecentlySuccessfulDeployment: FC<{ ); }; +const syncOptions = ["Sync", "Quick Sync", "Pipeline Sync"]; +const syncStrategyByIndex: SyncStrategy[] = [ + SyncStrategy.AUTO, + SyncStrategy.QUICK_SYNC, + SyncStrategy.PIPELINE, +]; + export const ApplicationDetail: FC = memo(function ApplicationDetail({ applicationId, }) { @@ -167,9 +175,14 @@ export const ApplicationDetail: FC = memo(function ApplicationDetail({ const isSyncing = useIsSyncingApplication(app?.id); - const handleSync = (): void => { + const handleSync = (index: number): void => { if (app) { - dispatch(syncApplication({ applicationId: app.id })); + dispatch( + syncApplication({ + applicationId: app.id, + syncStrategy: syncStrategyByIndex[index], + }) + ); } }; @@ -288,18 +301,14 @@ export const ApplicationDetail: FC = memo(function ApplicationDetail({ - + /> ); diff --git a/pkg/app/web/src/components/deployment-detail.tsx b/pkg/app/web/src/components/deployment-detail.tsx index 6c8dd1a0ec..fe26636893 100644 --- a/pkg/app/web/src/components/deployment-detail.tsx +++ b/pkg/app/web/src/components/deployment-detail.tsx @@ -221,6 +221,7 @@ export const DeploymentDetail: FC = memo(function DeploymentDetail({ { dispatch( cancelDeployment({ diff --git a/pkg/app/web/src/components/split-button.stories.tsx b/pkg/app/web/src/components/split-button.stories.tsx index 15416c8267..dd3bc6f582 100644 --- a/pkg/app/web/src/components/split-button.stories.tsx +++ b/pkg/app/web/src/components/split-button.stories.tsx @@ -10,6 +10,7 @@ export default { export const overview: React.FC = () => ( } options={["Cancel", "Cancel Without Rollback"]} onClick={action("onClick")} @@ -19,6 +20,7 @@ export const overview: React.FC = () => ( export const loading: React.FC = () => ( } options={["Cancel", "Cancel Without Rollback"]} onClick={action("onClick")} diff --git a/pkg/app/web/src/components/split-button.test.tsx b/pkg/app/web/src/components/split-button.test.tsx index 81cf773af7..8d74b214a8 100644 --- a/pkg/app/web/src/components/split-button.test.tsx +++ b/pkg/app/web/src/components/split-button.test.tsx @@ -7,6 +7,7 @@ it("calls onClick handler with option's index if clicked", () => { const onClick = jest.fn(); render( { expect(onClick).toHaveBeenCalledWith(0); act(() => { - userEvent.click( - screen.getByRole("button", { name: "select merge strategy" }) - ); + userEvent.click(screen.getByRole("button", { name: "select option" })); }); userEvent.click(screen.getByRole("menuitem", { name: "option2" })); userEvent.click(screen.getByRole("button", { name: "option2" })); diff --git a/pkg/app/web/src/components/split-button.tsx b/pkg/app/web/src/components/split-button.tsx index 33485cba90..46b067f1f4 100644 --- a/pkg/app/web/src/components/split-button.tsx +++ b/pkg/app/web/src/components/split-button.tsx @@ -1,6 +1,7 @@ import { Button, ButtonGroup, + PropTypes, CircularProgress, ClickAwayListener, Grow, @@ -26,9 +27,11 @@ const useStyles = makeStyles((theme) => ({ interface Props { options: string[]; + label: string; onClick: (index: number) => void; startIcon?: React.ReactNode; loading: boolean; + color?: PropTypes.Color; className?: string; } @@ -38,6 +41,8 @@ export const SplitButton: FC = ({ loading, startIcon, className, + color, + label, }) => { const classes = useStyles(); const anchorRef = useRef(null); @@ -48,7 +53,7 @@ export const SplitButton: FC = ({
@@ -66,7 +71,7 @@ export const SplitButton: FC = ({ size="small" aria-controls={openCancelMenu ? "split-button-menu" : undefined} aria-expanded={openCancelMenu ? "true" : undefined} - aria-label="select merge strategy" + aria-label={label} aria-haspopup="menu" onClick={() => setOpenCancelMenu(!openCancelMenu)} > diff --git a/pkg/app/web/src/modules/applications.ts b/pkg/app/web/src/modules/applications.ts index 72df1d0869..d24007d31d 100644 --- a/pkg/app/web/src/modules/applications.ts +++ b/pkg/app/web/src/modules/applications.ts @@ -13,7 +13,7 @@ import { ApplicationGitRepository, ApplicationKind, } from "pipe/pkg/app/web/model/common_pb"; -import { SyncStrategy } from "pipe/pkg/app/web/model/deployment_pb"; +import { SyncStrategy } from "./deployments"; import { fetchCommand, CommandStatus, CommandModel } from "./commands"; import { AppState } from "."; @@ -51,12 +51,9 @@ export const fetchApplication = createAsyncThunk< export const syncApplication = createAsyncThunk< void, - { applicationId: string } ->("applications/sync", async ({ applicationId }, thunkAPI) => { - const { commandId } = await applicationsAPI.syncApplication({ - applicationId: applicationId, - syncStrategy: SyncStrategy.AUTO, - }); + { applicationId: string; syncStrategy: SyncStrategy } +>("applications/sync", async (values, thunkAPI) => { + const { commandId } = await applicationsAPI.syncApplication(values); await thunkAPI.dispatch(fetchCommand(commandId)); }); diff --git a/pkg/app/web/src/modules/deployments.ts b/pkg/app/web/src/modules/deployments.ts index 7c5b9d2059..11f0894716 100644 --- a/pkg/app/web/src/modules/deployments.ts +++ b/pkg/app/web/src/modules/deployments.ts @@ -233,4 +233,5 @@ export const deploymentsSlice = createSlice({ export { DeploymentStatus, StageStatus, + SyncStrategy, } from "pipe/pkg/app/web/model/deployment_pb";