Skip to content

Commit

Permalink
Implement application enabling (#453)
Browse files Browse the repository at this point in the history
**What this PR does / why we need it**:

![image](https://user-images.githubusercontent.com/6136383/87662753-a299c200-c79d-11ea-885b-a5bfa3f93d5e.png)


**Which issue(s) this PR fixes**:

Fixes #327

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
-->
```release-note
NONE
```

This PR was merged by Kapetanios.
  • Loading branch information
cakecatz committed Jul 16, 2020
1 parent 5eb2eff commit 2c04439
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 152 deletions.
12 changes: 12 additions & 0 deletions pkg/app/web/src/api/applications.ts
Expand Up @@ -12,6 +12,8 @@ import {
SyncApplicationResponse,
DisableApplicationRequest,
DisableApplicationResponse,
EnableApplicationRequest,
EnableApplicationResponse,
} from "pipe/pkg/app/web/api_client/service_pb";
import { ApplicationGitPath } from "pipe/pkg/app/web/model/common_pb";
import * as google_protobuf_wrappers_pb from "google-protobuf/google/protobuf/wrappers_pb";
Expand Down Expand Up @@ -102,3 +104,13 @@ export const disableApplication = async ({
req.setApplicationId(applicationId);
return apiRequest(req, apiClient.disableApplication);
};

export const enableApplication = async ({
applicationId,
}: EnableApplicationRequest.AsObject): Promise<
EnableApplicationResponse.AsObject
> => {
const req = new EnableApplicationRequest();
req.setApplicationId(applicationId);
return apiRequest(req, apiClient.enableApplication);
};
154 changes: 50 additions & 104 deletions pkg/app/web/src/components/application-filter.tsx
Expand Up @@ -8,11 +8,16 @@ import {
Select,
Typography,
} from "@material-ui/core";
import React, { FC, useReducer, memo, useEffect } from "react";
import { useSelector } from "react-redux";
import React, { FC, memo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { APPLICATION_KIND_TEXT } from "../constants/application-kind";
import { APPLICATION_SYNC_STATUS_TEXT } from "../constants/application-sync-status-text";
import { AppState } from "../modules";
import {
ApplicationFilterOptions,
clearApplicationFilter,
updateApplicationFilter,
} from "../modules/application-filter-options";
import {
ApplicationKind,
ApplicationKindKey,
Expand Down Expand Up @@ -47,98 +52,34 @@ const useStyles = makeStyles((theme) => ({
},
}));

const ALL_VALUE = "ALL";

type ActiveStatus = typeof ALL_VALUE | "enabled" | "disabled";

interface FormState {
syncStatus: ApplicationSyncStatus | typeof ALL_VALUE;
applicationKind: ApplicationKind | typeof ALL_VALUE;
activeStatus: ActiveStatus;
env: string;
}

const initialState: FormState = {
activeStatus: "enabled",
syncStatus: ALL_VALUE,
env: ALL_VALUE,
applicationKind: ALL_VALUE,
};

type Actions =
| {
type: "update-sync-status";
value: ApplicationSyncStatus | typeof ALL_VALUE;
}
| {
type: "update-application-kind";
value: ApplicationKind | typeof ALL_VALUE;
}
| { type: "update-env"; value: string }
| { type: "update-active-status"; value: ActiveStatus }
| { type: "clear-form" };
const reducer = (state: FormState, action: Actions): FormState => {
switch (action.type) {
case "update-active-status":
return { ...state, activeStatus: action.value };
case "update-sync-status":
return { ...state, syncStatus: action.value };
case "update-application-kind":
return { ...state, applicationKind: action.value };
case "update-env":
return { ...state, env: action.value };
case "clear-form":
return initialState;
}
};

interface Options {
enabled?: {
value: boolean;
};
kindsList: ApplicationKind[];
envIdsList: string[];
syncStatusesList: ApplicationSyncStatus[];
}

interface Props {
open: boolean;
onChange: (props: Options) => void;
onChange: () => void;
}

const ALL_VALUE = "ALL";
const getActiveStatusText = (v: boolean): string =>
v ? "enabled" : "disabled";

export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
open,
onChange,
}) {
const classes = useStyles();
const dispatch = useDispatch();
const envs = useSelector<AppState, Environment[]>((state) =>
selectAll(state.environments)
);
const options = useSelector<AppState, ApplicationFilterOptions>(
(state) => state.applicationFilterOptions
);

const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
const options: Options = {
kindsList: [],
envIdsList: [],
syncStatusesList: [],
};
if (state.activeStatus !== ALL_VALUE) {
options.enabled = {
value: state.activeStatus === "enabled",
};
}
if (state.applicationKind !== ALL_VALUE) {
options.kindsList = [state.applicationKind];
}
if (state.env !== ALL_VALUE) {
options.envIdsList = [state.env];
}
if (state.syncStatus !== ALL_VALUE) {
options.syncStatusesList = [state.syncStatus];
}
onChange(options);
}, [state, onChange]);
const handleUpdateFilterValue = (
options: Partial<ApplicationFilterOptions>
): void => {
dispatch(updateApplicationFilter(options));
onChange();
};

if (open === false) {
return null;
Expand All @@ -151,7 +92,8 @@ export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
<Button
color="primary"
onClick={() => {
dispatch({ type: "clear-form" });
dispatch(clearApplicationFilter());
onChange();
}}
>
Clear
Expand All @@ -163,13 +105,13 @@ export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
<Select
labelId="filter-env"
id="filter-env"
value={state.env}
value={options.envIdsList[0] || ALL_VALUE}
label="Environment"
className={classes.select}
onChange={(e) => {
dispatch({
type: "update-env",
value: e.target.value as string,
handleUpdateFilterValue({
envIdsList:
e.target.value === ALL_VALUE ? [] : [e.target.value as string],
});
}}
>
Expand All @@ -189,16 +131,15 @@ export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
<Select
labelId="filter-application-kind"
id="filter-application-kind"
value={state.applicationKind}
value={options.kindsList[0] || ALL_VALUE}
label="Application Kind"
className={classes.select}
onChange={(e) => {
dispatch({
type: "update-application-kind",
value:
e.target.value === ""
? ALL_VALUE
: (e.target.value as ApplicationKind),
handleUpdateFilterValue({
kindsList:
e.target.value === ALL_VALUE
? []
: [e.target.value as ApplicationKind],
});
}}
>
Expand Down Expand Up @@ -226,16 +167,15 @@ export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
<Select
labelId="filter-sync-status"
id="filter-sync-status"
value={state.syncStatus}
value={options.syncStatusesList[0] || ALL_VALUE}
label="Sync Status"
className={classes.select}
onChange={(e) => {
dispatch({
type: "update-sync-status",
value:
e.target.value === ""
? ALL_VALUE
: (e.target.value as ApplicationSyncStatus),
handleUpdateFilterValue({
syncStatusesList:
e.target.value === ALL_VALUE
? []
: [e.target.value as ApplicationSyncStatus],
});
}}
>
Expand Down Expand Up @@ -263,13 +203,19 @@ export const ApplicationFilter: FC<Props> = memo(function ApplicationFilter({
<Select
labelId="filter-active-status"
id="filter-active-status"
value={state.activeStatus}
value={
options.enabled === undefined
? ALL_VALUE
: getActiveStatusText(options.enabled.value)
}
label="Active Status"
className={classes.select}
onChange={(e) => {
dispatch({
type: "update-active-status",
value: e.target.value as ActiveStatus,
handleUpdateFilterValue({
enabled:
e.target.value === ALL_VALUE
? undefined
: { value: e.target.value === "enabled" },
});
}}
>
Expand Down
67 changes: 46 additions & 21 deletions pkg/app/web/src/components/application-list.tsx
Expand Up @@ -17,20 +17,22 @@ import MenuIcon from "@material-ui/icons/MoreVert";
import { Dictionary } from "@reduxjs/toolkit";
import dayjs from "dayjs";
import React, { FC, memo, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { Link as RouterLink } from "react-router-dom";
import { PAGE_PATH_APPLICATIONS } from "../constants";
import { APPLICATION_SYNC_STATUS_TEXT } from "../constants/application-sync-status-text";
import { AppState } from "../modules";
import {
Application,
selectAll,
enableApplication,
fetchApplications,
selectAll,
} from "../modules/applications";
import {
Environment,
selectEntities as selectEnvs,
} from "../modules/environments";
import { AppDispatch } from "../store";
import { DisableApplicationDialog } from "./disable-application-dialog";
import { SyncStatusIcon } from "./sync-status-icon";

Expand Down Expand Up @@ -62,29 +64,46 @@ const EmptyDeploymentData: FC = () => (

export const ApplicationList: FC = memo(function ApplicationList() {
const classes = useStyles();
const dispatch = useDispatch();
const dispatch = useDispatch<AppDispatch>();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const isOpenMenu = Boolean(anchorEl);
const [disableTarget, setDisableTarget] = useState<null | string>(null);
const [actionTarget, setActionTarget] = useState<Application | null>(null);
const [openDisableDialog, setOpenDisableDialog] = useState(false);

const applications = useSelector<AppState, Application[]>((state) =>
selectAll(state.applications)
);
const envs = useSelector<AppState, Dictionary<Environment>>((state) =>
selectEnvs(state.environments)
);

const closeMenu = (): void => {
setAnchorEl(null);
setTimeout(() => {
setActionTarget(null);
}, 200);
};

// Menu item event handler
const handleOnClickDisable = (): void => {
if (anchorEl?.dataset.id) {
setDisableTarget(anchorEl.dataset.id);
}
setAnchorEl(null);
setOpenDisableDialog(true);
};
const handleOnCancelDisable = (): void => {
setDisableTarget(null);
dispatch(fetchApplications());

const handleOnClickEnable = (): void => {
if (actionTarget) {
dispatch(enableApplication({ applicationId: actionTarget.id })).then(
() => {
dispatch(fetchApplications());
}
);
}
closeMenu();
};
const handleOnDisable = (): void => {
setAnchorEl(null);
setDisableTarget(null);

const handleCloseDialog = (): void => {
closeMenu();
setOpenDisableDialog(false);
dispatch(fetchApplications());
};

Expand Down Expand Up @@ -152,7 +171,10 @@ export const ApplicationList: FC = memo(function ApplicationList() {
<TableCell align="right">
<IconButton
data-id={app.id}
onClick={(e) => setAnchorEl(e.currentTarget)}
onClick={(e) => {
setAnchorEl(e.currentTarget);
setActionTarget(app);
}}
>
<MenuIcon />
</IconButton>
Expand All @@ -169,22 +191,25 @@ export const ApplicationList: FC = memo(function ApplicationList() {
anchorEl={anchorEl}
keepMounted
open={isOpenMenu}
onClose={() => {
setAnchorEl(null);
}}
onClose={closeMenu}
PaperProps={{
style: {
width: "20ch",
},
}}
>
<MenuItem onClick={handleOnClickDisable}>Disable</MenuItem>
{actionTarget && actionTarget.disabled ? (
<MenuItem onClick={handleOnClickEnable}>Enable</MenuItem>
) : (
<MenuItem onClick={handleOnClickDisable}>Disable</MenuItem>
)}
</Menu>

<DisableApplicationDialog
applicationId={disableTarget}
onDisable={handleOnDisable}
onCancel={handleOnCancelDisable}
open={openDisableDialog}
applicationId={actionTarget && actionTarget.id}
onDisable={handleCloseDialog}
onCancel={handleCloseDialog}
/>
</div>
);
Expand Down

0 comments on commit 2c04439

Please sign in to comment.