Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions pkg/app/web/src/api/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
EnableApplicationResponse,
UpdateApplicationRequest,
UpdateApplicationResponse,
DeleteApplicationRequest,
DeleteApplicationResponse,
} from "pipe/pkg/app/web/api_client/service_pb";
import { ApplicationGitPath } from "pipe/pkg/app/web/model/common_pb";
import { ApplicationGitRepository } from "pipe/pkg/app/web/model/common_pb";
Expand Down Expand Up @@ -137,8 +139,6 @@ export const updateApplication = async ({
}: Required<UpdateApplicationRequest.AsObject>): Promise<
UpdateApplicationResponse.AsObject
> => {
console.log(UpdateApplicationRequest);

const req = new UpdateApplicationRequest();
req.setApplicationId(applicationId);
req.setName(name);
Expand All @@ -161,3 +161,13 @@ export const updateApplication = async ({
req.setGitPath(appGitPath);
return apiRequest(req, apiClient.updateApplication);
};

export const deleteApplication = async ({
applicationId,
}: DeleteApplicationRequest.AsObject): Promise<
DeleteApplicationResponse.AsObject
> => {
const req = new DeleteApplicationRequest();
req.setApplicationId(applicationId);
return apiRequest(req, apiClient.deleteApplication);
};
18 changes: 15 additions & 3 deletions pkg/app/web/src/components/application-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { SyncStatusIcon } from "./sync-status-icon";
import { SealedSecretDialog } from "./sealed-secret-dialog";
import { APPLICATION_KIND_TEXT } from "../constants/application-kind";
import { setUpdateTargetId } from "../modules/update-application";
import { DeleteApplicationDialog } from "./delete-application-dialog";
import { setDeletingAppId } from "../modules/delete-application";

const useStyles = makeStyles((theme) => ({
root: {
Expand Down Expand Up @@ -148,6 +150,13 @@ export const ApplicationList: FC = memo(function ApplicationList() {
closeMenu();
}, [dispatch, actionTarget]);

const handleDeleteClick = useCallback(() => {
if (actionTarget) {
dispatch(setDeletingAppId(actionTarget.id));
}
closeMenu();
}, [actionTarget, dispatch]);

return (
<div className={classes.root}>
<TableContainer component={Paper}>
Expand Down Expand Up @@ -268,14 +277,15 @@ export const ApplicationList: FC = memo(function ApplicationList() {
}}
>
<MenuItem onClick={handleEditClick}>Edit</MenuItem>
<MenuItem onClick={handleOnClickGenerateSecret}>
Encrypt Secret
</MenuItem>
{actionTarget && actionTarget.disabled ? (
<MenuItem onClick={handleOnClickEnable}>Enable</MenuItem>
) : (
<MenuItem onClick={handleOnClickDisable}>Disable</MenuItem>
)}
<MenuItem onClick={handleOnClickGenerateSecret}>
Encrypt Secret
</MenuItem>
<MenuItem onClick={handleDeleteClick}>Delete</MenuItem>
</Menu>

<DisableApplicationDialog
Expand All @@ -290,6 +300,8 @@ export const ApplicationList: FC = memo(function ApplicationList() {
applicationId={actionTarget && actionTarget.id}
onClose={handleOnCloseGenerateDialog}
/>

<DeleteApplicationDialog />
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";
import { createDecoratorRedux } from "../../.storybook/redux-decorator";
import { dummyApplication } from "../__fixtures__/dummy-application";
import { DeleteApplicationDialog } from "./delete-application-dialog";

export default {
title: "DeleteApplicationDialog",
component: DeleteApplicationDialog,
decorators: [
createDecoratorRedux({
applications: {
entities: {
[dummyApplication.id]: dummyApplication,
},
ids: [dummyApplication.id],
},
deleteApplication: {
applicationId: dummyApplication.id,
deleting: false,
},
}),
],
};

export const overview: React.FC = () => <DeleteApplicationDialog />;
123 changes: 123 additions & 0 deletions pkg/app/web/src/components/delete-application-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { FC, memo, useCallback } from "react";
import {
makeStyles,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
CircularProgress,
} from "@material-ui/core";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import {
selectById,
Application,
fetchApplications,
} from "../modules/applications";
import {
clearDeletingApp,
deleteApplication,
} from "../modules/delete-application";
import { AppState } from "../modules";
import Alert from "@material-ui/lab/Alert";
import { AppDispatch } from "../store";
import { red } from "@material-ui/core/colors";
import { UI_TEXT_CANCEL, UI_TEXT_DELETE } from "../constants/ui-text";
import { useStyles as useButtonStyles } from "../styles/button";
import { Skeleton } from "@material-ui/lab";
import { addToast } from "../modules/toasts";
import { DELETE_APPLICATION_SUCCESS } from "../constants/toast-text";

const useStyles = makeStyles((theme) => ({
applicationName: {
color: theme.palette.text.primary,
fontWeight: theme.typography.fontWeightMedium,
},
description: {
marginBottom: theme.spacing(2),
},
deleteButton: {
color: theme.palette.getContrastText(red[400]),
backgroundColor: red[700],
"&:hover": {
backgroundColor: red[700],
},
},
}));

const TITLE = "Delete Application";
const ALERT_TEXT = "Are you sure you want to delete the application?";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone has a better phrase, please let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.
"Are you sure you want to DELETE the application?"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could you the same text as github does 🤔
Screen Shot 2020-12-22 at 19 08 46

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirmation could be added/improved later.
Let's merge this pull request with the current design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙆‍♀️ 🚀


export const DeleteApplicationDialog: FC = memo(
function DeleteApplicationDialog() {
const classes = useStyles();
const buttonClasses = useButtonStyles();
const dispatch = useDispatch<AppDispatch>();

const [application, isDeleting] = useSelector<
AppState,
[Application | undefined, boolean]
>(
(state) => [
state.deleteApplication.applicationId
? selectById(
state.applications,
state.deleteApplication.applicationId
)
: undefined,
state.deleteApplication.deleting,
],
shallowEqual
);

const handleDelete = useCallback(() => {
dispatch(deleteApplication()).then(() => {
dispatch(fetchApplications());
dispatch(
addToast({ severity: "success", message: DELETE_APPLICATION_SUCCESS })
);
});
}, [dispatch]);

const handleCancel = useCallback(() => {
dispatch(clearDeletingApp());
}, [dispatch]);

return (
<Dialog open={Boolean(application)} disableBackdropClick={isDeleting}>
<DialogTitle>{TITLE}</DialogTitle>
<DialogContent>
<Alert severity="error" className={classes.description}>
{ALERT_TEXT}
</Alert>
<Typography variant="caption">Name</Typography>
<Typography variant="body1" className={classes.applicationName}>
{application ? (
application.name
) : (
<Skeleton height={24} width={200} />
)}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={handleCancel} disabled={isDeleting}>
{UI_TEXT_CANCEL}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleDelete}
className={classes.deleteButton}
disabled={isDeleting}
>
{UI_TEXT_DELETE}
{isDeleting && (
<CircularProgress size={24} className={buttonClasses.progress} />
)}
</Button>
</DialogActions>
</Dialog>
);
}
);
1 change: 1 addition & 0 deletions pkg/app/web/src/components/toasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const Toasts: FC = () => {
<MuiAlert
onClose={handleClose}
severity={item.severity}
elevation={6}
action={
item.to ? (
<Button
Expand Down
3 changes: 3 additions & 0 deletions pkg/app/web/src/constants/toast-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export const COPY_API_KEY = "API Key copied to clipboard.";
// Piped
export const ADD_PIPED_SUCCESS = "Successfully added Piped.";
export const UPDATE_PIPED_SUCCESS = "Successfully updated Piped.";

// Application
export const DELETE_APPLICATION_SUCCESS = "Successfully deleted Application.";
1 change: 1 addition & 0 deletions pkg/app/web/src/constants/ui-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const UI_TEXT_ADD = "Add";
export const UI_TEXT_EDIT = "Edit";
export const UI_TEXT_REFRESH = "REFRESH";
export const UI_TEXT_CLOSE = "Close";
export const UI_TEXT_DELETE = "Delete";
3 changes: 2 additions & 1 deletion pkg/app/web/src/modules/applications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
fetchApplication,
fetchApplications,
syncApplication,
ApplicationsState,
} from "./applications";
import { CommandModel, CommandStatus, fetchCommand } from "./commands";
import * as applicationsAPI from "../api/applications";

const baseState = {
const baseState: ApplicationsState = {
adding: false,
disabling: {},
entities: {},
Expand Down
30 changes: 17 additions & 13 deletions pkg/app/web/src/modules/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,25 @@ export const enableApplication = createAsyncThunk<
await applicationsAPI.enableApplication(props);
});

const initialState = applicationsAdapter.getInitialState<{
adding: boolean;
loading: boolean;
syncing: Record<string, boolean>;
disabling: Record<string, boolean>;
fetchApplicationError: SerializedError | null;
}>({
adding: false,
loading: false,
syncing: {},
disabling: {},
fetchApplicationError: null,
});

export type ApplicationsState = typeof initialState;

export const applicationsSlice = createSlice({
name: "applications",
initialState: applicationsAdapter.getInitialState<{
adding: boolean;
loading: boolean;
syncing: Record<string, boolean>;
disabling: Record<string, boolean>;
fetchApplicationError: SerializedError | null;
}>({
adding: false,
loading: false,
syncing: {},
disabling: {},
fetchApplicationError: null,
}),
initialState,
reducers: {},
extraReducers: (builder) => {
builder
Expand Down
19 changes: 19 additions & 0 deletions pkg/app/web/src/modules/delete-application.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
deleteApplicationSlice,
DeleteApplicationState,
} from "./delete-application";

const initialState: DeleteApplicationState = {
applicationId: null,
deleting: false,
};

describe("deleteApplicationSlice reducer", () => {
it("should return the initial state", () => {
expect(
deleteApplicationSlice.reducer(undefined, {
type: "TEST_ACTION",
})
).toEqual(initialState);
});
});
60 changes: 60 additions & 0 deletions pkg/app/web/src/modules/delete-application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppState } from ".";
import * as applicationsAPI from "../api/applications";

export interface DeleteApplicationState {
applicationId: string | null;
deleting: boolean;
}

const initialState: DeleteApplicationState = {
applicationId: null,
deleting: false,
};

export const deleteApplication = createAsyncThunk<
void,
void,
{
state: AppState;
}
>("applications/delete", async (_, thunkAPI) => {
const state = thunkAPI.getState();

if (state.deleteApplication.applicationId) {
await applicationsAPI.deleteApplication({
applicationId: state.deleteApplication.applicationId,
});
}
});

export const deleteApplicationSlice = createSlice({
name: "deleteApplication",
initialState,
reducers: {
setDeletingAppId(state, action: PayloadAction<string>) {
state.applicationId = action.payload;
},
clearDeletingApp(state) {
state.applicationId = null;
},
},
extraReducers: (builder) => {
builder
.addCase(deleteApplication.pending, (state) => {
state.deleting = true;
})
.addCase(deleteApplication.rejected, (state) => {
state.deleting = false;
})
.addCase(deleteApplication.fulfilled, (state) => {
state.deleting = false;
state.applicationId = null;
});
},
});

export const {
clearDeletingApp,
setDeletingAppId,
} = deleteApplicationSlice.actions;
Loading