You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/11/09 21:25:52 UTC
[incubator-superset] branch master updated: feat: annotation delete
modal, bulk delete and empty state (#11540)
This is an automated email from the ASF dual-hosted git repository.
tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new dda95ed feat: annotation delete modal, bulk delete and empty state (#11540)
dda95ed is described below
commit dda95ed2504d3ade9eb365b8da36ae692353242f
Author: Lily Kuang <li...@preset.io>
AuthorDate: Mon Nov 9 13:25:16 2020 -0800
feat: annotation delete modal, bulk delete and empty state (#11540)
---
.../views/CRUD/annotation/AnnotationList_spec.jsx | 52 ++++++-
.../src/common/components/common.stories.tsx | 13 ++
superset-frontend/src/components/Menu/SubMenu.tsx | 2 +
.../src/views/CRUD/annotation/AnnotationList.tsx | 153 ++++++++++++++++++---
.../src/views/CRUD/annotation/AnnotationModal.tsx | 2 +-
.../CRUD/annotationlayers/AnnotationLayersList.tsx | 2 +-
6 files changed, 200 insertions(+), 24 deletions(-)
diff --git a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
index 80c3562..1fc7089 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
@@ -23,9 +23,13 @@ import fetchMock from 'fetch-mock';
import { styledMount as mount } from 'spec/helpers/theming';
import AnnotationList from 'src/views/CRUD/annotation/AnnotationList';
-import SubMenu from 'src/components/Menu/SubMenu';
+import DeleteModal from 'src/components/DeleteModal';
+import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import ListView from 'src/components/ListView';
+import SubMenu from 'src/components/Menu/SubMenu';
+
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { act } from 'react-dom/test-utils';
// store needed for withToasts(AnnotationList)
const mockStore = configureStore([thunk]);
@@ -34,7 +38,9 @@ const store = mockStore({});
const annotationsEndpoint = 'glob:*/api/v1/annotation_layer/*/annotation*';
const annotationLayerEndpoint = 'glob:*/api/v1/annotation_layer/*';
-const mockannotation = [...new Array(3)].map((_, i) => ({
+fetchMock.delete(annotationsEndpoint, {});
+
+const mockannotations = [...new Array(3)].map((_, i) => ({
changed_on_delta_humanized: `${i} day(s) ago`,
created_by: {
first_name: `user`,
@@ -53,7 +59,7 @@ const mockannotation = [...new Array(3)].map((_, i) => ({
fetchMock.get(annotationsEndpoint, {
ids: [2, 0, 1],
- result: mockannotation,
+ result: mockannotations,
count: 3,
});
@@ -110,4 +116,44 @@ describe('AnnotationList', () => {
`"http://localhost/api/v1/annotation_layer/1/annotation/?q=(order_column:short_descr,order_direction:desc,page:0,page_size:25)"`,
);
});
+
+ it('renders a DeleteModal', () => {
+ expect(wrapper.find(DeleteModal)).toExist();
+ });
+
+ it('deletes', async () => {
+ act(() => {
+ wrapper.find('[data-test="delete-action"]').first().props().onClick();
+ });
+ await waitForComponentToPaint(wrapper);
+
+ expect(
+ wrapper.find(DeleteModal).first().props().description,
+ ).toMatchInlineSnapshot(
+ `"Are you sure you want to delete annotation 0 label?"`,
+ );
+
+ act(() => {
+ wrapper
+ .find('#delete')
+ .first()
+ .props()
+ .onChange({ target: { value: 'DELETE' } });
+ });
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper.find('button').last().props().onClick();
+ });
+ });
+
+ it('shows/hides bulk actions when bulk actions is clicked', async () => {
+ const button = wrapper.find('[data-test="annotation-bulk-select"]').first();
+ act(() => {
+ button.props().onClick();
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
+ mockannotations.length + 1, // 1 for each row and 1 for select all
+ );
+ });
});
diff --git a/superset-frontend/src/common/components/common.stories.tsx b/superset-frontend/src/common/components/common.stories.tsx
index 19f597e..7bdae99 100644
--- a/superset-frontend/src/common/components/common.stories.tsx
+++ b/superset-frontend/src/common/components/common.stories.tsx
@@ -27,6 +27,10 @@ import AntdTooltip from './Tooltip';
import { Menu } from '.';
import { Dropdown } from './Dropdown';
import InfoTooltip from './InfoTooltip';
+import {
+ DatePicker as AntdDatePicker,
+ RangePicker as AntdRangePicker,
+} from './DatePicker';
export default {
title: 'Common Components',
@@ -224,3 +228,12 @@ StyledInfoTooltip.argTypes = {
},
},
};
+
+export const DatePicker = () => <AntdDatePicker showTime />;
+export const DateRangePicker = () => (
+ <AntdRangePicker
+ format="YYYY-MM-DD hh:mm a"
+ showTime={{ format: 'hh:mm a' }}
+ use12Hours
+ />
+);
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx
index cc6914d..00f3c85 100644
--- a/superset-frontend/src/components/Menu/SubMenu.tsx
+++ b/superset-frontend/src/components/Menu/SubMenu.tsx
@@ -86,6 +86,7 @@ type MenuChild = {
export interface ButtonProps {
name: ReactNode;
onClick: OnClickHandler;
+ 'data-test'?: string;
buttonStyle:
| 'primary'
| 'secondary'
@@ -159,6 +160,7 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
key={`${i}`}
buttonStyle={btn.buttonStyle}
onClick={btn.onClick}
+ data-test={btn['data-test']}
>
{btn.name}
</Button>
diff --git a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
index ee9b623..dba16e3 100644
--- a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
+++ b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
@@ -20,15 +20,20 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useParams, Link, useHistory } from 'react-router-dom';
import { t, styled, SupersetClient } from '@superset-ui/core';
-
import moment from 'moment';
+import rison from 'rison';
+
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
-import ListView from 'src/components/ListView';
+import Button from 'src/components/Button';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import DeleteModal from 'src/components/DeleteModal';
+import ListView, { ListViewProps } from 'src/components/ListView';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import getClientErrorObject from 'src/utils/getClientErrorObject';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import { IconName } from 'src/components/Icon';
import { useListViewResource } from 'src/views/CRUD/hooks';
+import { createErrorHandler } from 'src/views/CRUD/utils';
import { AnnotationObject } from './types';
import AnnotationModal from './AnnotationModal';
@@ -37,18 +42,24 @@ const PAGE_SIZE = 25;
interface AnnotationListProps {
addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
}
-function AnnotationList({ addDangerToast }: AnnotationListProps) {
+function AnnotationList({
+ addDangerToast,
+ addSuccessToast,
+}: AnnotationListProps) {
const { annotationLayerId }: any = useParams();
const {
state: {
loading,
resourceCount: annotationsCount,
resourceCollection: annotations,
+ bulkSelectEnabled,
},
fetchData,
refreshData,
+ toggleBulkSelect,
} = useListViewResource<AnnotationObject>(
`annotation_layer/${annotationLayerId}/annotation`,
t('annotation'),
@@ -63,8 +74,11 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
currentAnnotation,
setCurrentAnnotation,
] = useState<AnnotationObject | null>(null);
-
- const handleAnnotationEdit = (annotation: AnnotationObject) => {
+ const [
+ annotationCurrentlyDeleting,
+ setAnnotationCurrentlyDeleting,
+ ] = useState<AnnotationObject | null>(null);
+ const handleAnnotationEdit = (annotation: AnnotationObject | null) => {
setCurrentAnnotation(annotation);
setAnnotationModalOpen(true);
};
@@ -85,7 +99,44 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
[annotationLayerId],
);
- // get the owners of this slice
+ const handleAnnotationDelete = ({ id, short_descr }: AnnotationObject) => {
+ SupersetClient.delete({
+ endpoint: `/api/v1/annotation_layer/${annotationLayerId}/annotation/${id}`,
+ }).then(
+ () => {
+ refreshData();
+ setAnnotationCurrentlyDeleting(null);
+ addSuccessToast(t('Deleted: %s', short_descr));
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('There was an issue deleting %s: %s', short_descr, errMsg),
+ ),
+ ),
+ );
+ };
+
+ const handleBulkAnnotationsDelete = (
+ annotationsToDelete: AnnotationObject[],
+ ) => {
+ SupersetClient.delete({
+ endpoint: `/api/v1/annotation_layer/${annotationLayerId}/annotation/?q=${rison.encode(
+ annotationsToDelete.map(({ id }) => id),
+ )}`,
+ }).then(
+ ({ json = {} }) => {
+ refreshData();
+ addSuccessToast(json.message);
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('There was an issue deleting the selected annotations: %s', errMsg),
+ ),
+ ),
+ );
+ };
+
+ // get the Annotation Layer
useEffect(() => {
fetchAnnotationLayer();
}, [fetchAnnotationLayer]);
@@ -122,7 +173,7 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleAnnotationEdit(original);
- const handleDelete = () => {}; // openDatabaseDeleteModal(original);
+ const handleDelete = () => setAnnotationCurrentlyDeleting(original);
const actions = [
{
label: 'edit-action',
@@ -159,11 +210,17 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
),
buttonStyle: 'primary',
onClick: () => {
- setCurrentAnnotation(null);
- setAnnotationModalOpen(true);
+ handleAnnotationEdit(null);
},
});
+ subMenuButtons.push({
+ name: t('Bulk Select'),
+ onClick: toggleBulkSelect,
+ buttonStyle: 'secondary',
+ 'data-test': 'annotation-bulk-select',
+ });
+
const StyledHeader = styled.div`
display: flex;
flex-direction: row;
@@ -186,6 +243,24 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
hasHistory = false;
}
+ const EmptyStateButton = (
+ <Button
+ buttonStyle="primary"
+ onClick={() => {
+ handleAnnotationEdit(null);
+ }}
+ >
+ <>
+ <i className="fa fa-plus" /> {t('Annotation')}
+ </>
+ </Button>
+ );
+
+ const emptyState = {
+ message: t('No annotation yet'),
+ slot: EmptyStateButton,
+ };
+
return (
<>
<SubMenu
@@ -211,16 +286,56 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
annnotationLayerId={annotationLayerId}
onHide={() => setAnnotationModalOpen(false)}
/>
- <ListView<AnnotationObject>
- className="css-templates-list-view"
- columns={columns}
- count={annotationsCount}
- data={annotations}
- fetchData={fetchData}
- initialSort={initialSort}
- loading={loading}
- pageSize={PAGE_SIZE}
- />
+ {annotationCurrentlyDeleting && (
+ <DeleteModal
+ description={t(
+ `Are you sure you want to delete ${annotationCurrentlyDeleting?.short_descr}?`,
+ )}
+ onConfirm={() => {
+ if (annotationCurrentlyDeleting) {
+ handleAnnotationDelete(annotationCurrentlyDeleting);
+ }
+ }}
+ onHide={() => setAnnotationCurrentlyDeleting(null)}
+ open
+ title={t('Delete Annotation?')}
+ />
+ )}
+ <ConfirmStatusChange
+ title={t('Please confirm')}
+ description={t(
+ 'Are you sure you want to delete the selected annotations?',
+ )}
+ onConfirm={handleBulkAnnotationsDelete}
+ >
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [
+ {
+ key: 'delete',
+ name: t('Delete'),
+ onSelect: confirmDelete,
+ type: 'danger',
+ },
+ ];
+
+ return (
+ <ListView<AnnotationObject>
+ className="annotations-list-view"
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ columns={columns}
+ count={annotationsCount}
+ data={annotations}
+ disableBulkSelect={toggleBulkSelect}
+ emptyState={emptyState}
+ fetchData={fetchData}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ />
+ );
+ }}
+ </ConfirmStatusChange>
</>
);
}
diff --git a/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx b/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
index ddd1931..2befe09 100644
--- a/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
+++ b/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
@@ -287,9 +287,9 @@ const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
<span className="required">*</span>
</div>
<RangePicker
+ format="YYYY-MM-DD hh:mm a"
onChange={onDateChange}
showTime={{ format: 'hh:mm a' }}
- format="YYYY-MM-DD hh:mm a"
use12Hours
// @ts-ignore
value={
diff --git a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx
index 7c24b0f..11db5b3 100644
--- a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx
+++ b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx
@@ -320,7 +320,7 @@ function AnnotationLayersList({
);
const emptyState = {
- message: 'No annotation layers yet',
+ message: t('No annotation layers yet'),
slot: EmptyStateButton,
};