mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
feat: annotation layers modal + filters (#11494)
This commit is contained in:
parent
5d9560c408
commit
01ddbd0697
@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import AnnotationLayerModal from 'src/views/CRUD/annotationlayers/AnnotationLayerModal';
|
||||||
|
import Modal from 'src/common/components/Modal';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
|
||||||
|
const mockData = { id: 1, name: 'test', descr: 'test description' };
|
||||||
|
const FETCH_ANNOTATION_LAYER_ENDPOINT = 'glob:*/api/v1/annotation_layer/*';
|
||||||
|
const ANNOTATION_LAYER_PAYLOAD = { result: mockData };
|
||||||
|
|
||||||
|
fetchMock.get(FETCH_ANNOTATION_LAYER_ENDPOINT, ANNOTATION_LAYER_PAYLOAD);
|
||||||
|
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
addDangerToast: () => {},
|
||||||
|
onLayerAdd: jest.fn(() => []),
|
||||||
|
onHide: () => {},
|
||||||
|
show: true,
|
||||||
|
layer: mockData,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function mountAndWait(props = mockedProps) {
|
||||||
|
const mounted = mount(<AnnotationLayerModal show {...props} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(mounted);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AnnotationLayerModal', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
wrapper = await mountAndWait();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(wrapper.find(AnnotationLayerModal)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a Modal', () => {
|
||||||
|
expect(wrapper.find(Modal)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders add header when no layer is included', async () => {
|
||||||
|
const addWrapper = await mountAndWait({});
|
||||||
|
expect(
|
||||||
|
addWrapper.find('[data-test="annotation-layer-modal-title"]').text(),
|
||||||
|
).toEqual('Add Annotation Layer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders edit header when layer prop is included', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('[data-test="annotation-layer-modal-title"]').text(),
|
||||||
|
).toEqual('Edit Annotation Layer Properties');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders input element for name', () => {
|
||||||
|
expect(wrapper.find('input[name="name"]')).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders textarea element for description', () => {
|
||||||
|
expect(wrapper.find('textarea[name="descr"]')).toExist();
|
||||||
|
});
|
||||||
|
});
|
@ -23,14 +23,15 @@ import fetchMock from 'fetch-mock';
|
|||||||
import { styledMount as mount } from 'spec/helpers/theming';
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
|
||||||
import AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList';
|
import AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList';
|
||||||
|
import AnnotationLayerModal from 'src/views/CRUD/annotationlayers/AnnotationLayerModal';
|
||||||
import SubMenu from 'src/components/Menu/SubMenu';
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
import ListView from 'src/components/ListView';
|
import ListView from 'src/components/ListView';
|
||||||
// import Filters from 'src/components/ListView/Filters';
|
import Filters from 'src/components/ListView/Filters';
|
||||||
// import DeleteModal from 'src/components/DeleteModal';
|
// import DeleteModal from 'src/components/DeleteModal';
|
||||||
// import Button from 'src/components/Button';
|
// import Button from 'src/components/Button';
|
||||||
// import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
// import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
// import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
// store needed for withToasts(AnnotationLayersList)
|
// store needed for withToasts(AnnotationLayersList)
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
@ -39,7 +40,7 @@ const store = mockStore({});
|
|||||||
const layersInfoEndpoint = 'glob:*/api/v1/annotation_layer/_info*';
|
const layersInfoEndpoint = 'glob:*/api/v1/annotation_layer/_info*';
|
||||||
const layersEndpoint = 'glob:*/api/v1/annotation_layer/?*';
|
const layersEndpoint = 'glob:*/api/v1/annotation_layer/?*';
|
||||||
// const layerEndpoint = 'glob:*/api/v1/annotation_layer/*';
|
// const layerEndpoint = 'glob:*/api/v1/annotation_layer/*';
|
||||||
// const templatesRelatedEndpoint = 'glob:*/api/v1/annotation_layer/related/*';
|
const layersRelatedEndpoint = 'glob:*/api/v1/annotation_layer/related/*';
|
||||||
|
|
||||||
const mocklayers = [...new Array(3)].map((_, i) => ({
|
const mocklayers = [...new Array(3)].map((_, i) => ({
|
||||||
changed_on_delta_humanized: `${i} day(s) ago`,
|
changed_on_delta_humanized: `${i} day(s) ago`,
|
||||||
@ -63,14 +64,14 @@ fetchMock.get(layersEndpoint, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* fetchMock.delete(layerEndpoint, {});
|
/* fetchMock.delete(layerEndpoint, {});
|
||||||
fetchMock.delete(layersEndpoint, {});
|
fetchMock.delete(layersEndpoint, {}); */
|
||||||
|
|
||||||
fetchMock.get(layersRelatedEndpoint, {
|
fetchMock.get(layersRelatedEndpoint, {
|
||||||
created_by: {
|
created_by: {
|
||||||
count: 0,
|
count: 0,
|
||||||
result: [],
|
result: [],
|
||||||
},
|
},
|
||||||
}); */
|
});
|
||||||
|
|
||||||
describe('AnnotationLayersList', () => {
|
describe('AnnotationLayersList', () => {
|
||||||
const wrapper = mount(<AnnotationLayersList />, { context: { store } });
|
const wrapper = mount(<AnnotationLayersList />, { context: { store } });
|
||||||
@ -91,6 +92,10 @@ describe('AnnotationLayersList', () => {
|
|||||||
expect(wrapper.find(ListView)).toExist();
|
expect(wrapper.find(ListView)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders a modal', () => {
|
||||||
|
expect(wrapper.find(AnnotationLayerModal)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
it('fetches layers', () => {
|
it('fetches layers', () => {
|
||||||
const callsQ = fetchMock.calls(/annotation_layer\/\?q/);
|
const callsQ = fetchMock.calls(/annotation_layer\/\?q/);
|
||||||
expect(callsQ).toHaveLength(1);
|
expect(callsQ).toHaveLength(1);
|
||||||
@ -98,4 +103,20 @@ describe('AnnotationLayersList', () => {
|
|||||||
`"http://localhost/api/v1/annotation_layer/?q=(order_column:name,order_direction:desc,page:0,page_size:25)"`,
|
`"http://localhost/api/v1/annotation_layer/?q=(order_column:name,order_direction:desc,page:0,page_size:25)"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders Filters', () => {
|
||||||
|
expect(wrapper.find(Filters)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searches', async () => {
|
||||||
|
const filtersWrapper = wrapper.find(Filters);
|
||||||
|
act(() => {
|
||||||
|
filtersWrapper.find('[name="name"]').first().props().onSubmit('foo');
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
|
||||||
|
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
|
||||||
|
`"http://localhost/api/v1/annotation_layer/?q=(filters:!((col:name,opr:ct,value:foo)),order_column:name,order_direction:desc,page:0,page_size:25)"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,6 @@ import {
|
|||||||
import { ListViewError, useListViewState } from './utils';
|
import { ListViewError, useListViewState } from './utils';
|
||||||
|
|
||||||
const ListViewStyles = styled.div`
|
const ListViewStyles = styled.div`
|
||||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.superset-list-view {
|
.superset-list-view {
|
||||||
@ -58,6 +57,7 @@ const ListViewStyles = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.body {
|
.body {
|
||||||
|
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-empty {
|
.ant-empty {
|
||||||
|
@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { FunctionComponent, useState, useEffect } from 'react';
|
||||||
|
import { styled, t } from '@superset-ui/core';
|
||||||
|
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||||
|
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import Modal from 'src/common/components/Modal';
|
||||||
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
|
|
||||||
|
import { AnnotationLayerObject } from './types';
|
||||||
|
|
||||||
|
interface AnnotationLayerModalProps {
|
||||||
|
addDangerToast: (msg: string) => void;
|
||||||
|
layer?: AnnotationLayerObject | null;
|
||||||
|
onLayerAdd?: (layer?: AnnotationLayerObject) => void;
|
||||||
|
onHide: () => void;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledAnnotationLayerTitle = styled.div`
|
||||||
|
margin: ${({ theme }) => theme.gridUnit * 2}px auto
|
||||||
|
${({ theme }) => theme.gridUnit * 4}px auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled(Icon)`
|
||||||
|
margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LayerContainer = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.gridUnit * 10}px;
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
margin-left: ${({ theme }) => theme.gridUnit / 2}px;
|
||||||
|
color: ${({ theme }) => theme.colors.error.base};
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea,
|
||||||
|
input[type='text'] {
|
||||||
|
padding: ${({ theme }) => theme.gridUnit * 1.5}px
|
||||||
|
${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AnnotationLayerModal: FunctionComponent<AnnotationLayerModalProps> = ({
|
||||||
|
addDangerToast,
|
||||||
|
onLayerAdd,
|
||||||
|
onHide,
|
||||||
|
show,
|
||||||
|
layer = null,
|
||||||
|
}) => {
|
||||||
|
const [disableSave, setDisableSave] = useState<boolean>(true);
|
||||||
|
const [
|
||||||
|
currentLayer,
|
||||||
|
setCurrentLayer,
|
||||||
|
] = useState<AnnotationLayerObject | null>();
|
||||||
|
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||||
|
const isEditMode = layer !== null;
|
||||||
|
|
||||||
|
// annotation layer fetch logic
|
||||||
|
const {
|
||||||
|
state: { loading, resource },
|
||||||
|
fetchResource,
|
||||||
|
createResource,
|
||||||
|
updateResource,
|
||||||
|
} = useSingleViewResource<AnnotationLayerObject>(
|
||||||
|
'annotation_layer',
|
||||||
|
t('annotation_layer'),
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const hide = () => {
|
||||||
|
setIsHidden(true);
|
||||||
|
onHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
if (isEditMode) {
|
||||||
|
// Edit
|
||||||
|
if (currentLayer && currentLayer.id) {
|
||||||
|
const update_id = currentLayer.id;
|
||||||
|
delete currentLayer.id;
|
||||||
|
delete currentLayer.created_by;
|
||||||
|
updateResource(update_id, currentLayer).then(() => {
|
||||||
|
if (onLayerAdd) {
|
||||||
|
onLayerAdd();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (currentLayer) {
|
||||||
|
// Create
|
||||||
|
createResource(currentLayer).then(() => {
|
||||||
|
if (onLayerAdd) {
|
||||||
|
onLayerAdd();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTextChange = (
|
||||||
|
event:
|
||||||
|
| React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
| React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const { target } = event;
|
||||||
|
const data = {
|
||||||
|
...currentLayer,
|
||||||
|
name: currentLayer ? currentLayer.name : '',
|
||||||
|
descr: currentLayer ? currentLayer.descr : '',
|
||||||
|
};
|
||||||
|
|
||||||
|
data[target.name] = target.value;
|
||||||
|
setCurrentLayer(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (currentLayer && currentLayer.name.length) {
|
||||||
|
setDisableSave(false);
|
||||||
|
} else {
|
||||||
|
setDisableSave(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
if (
|
||||||
|
isEditMode &&
|
||||||
|
(!currentLayer ||
|
||||||
|
!currentLayer.id ||
|
||||||
|
(layer && layer.id !== currentLayer.id) ||
|
||||||
|
(isHidden && show))
|
||||||
|
) {
|
||||||
|
if (layer && layer.id !== null && !loading) {
|
||||||
|
const id = layer.id || 0;
|
||||||
|
|
||||||
|
fetchResource(id).then(() => {
|
||||||
|
setCurrentLayer(resource);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!isEditMode &&
|
||||||
|
(!currentLayer || currentLayer.id || (isHidden && show))
|
||||||
|
) {
|
||||||
|
setCurrentLayer({
|
||||||
|
name: '',
|
||||||
|
descr: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
useEffect(() => {
|
||||||
|
validate();
|
||||||
|
}, [
|
||||||
|
currentLayer ? currentLayer.name : '',
|
||||||
|
currentLayer ? currentLayer.descr : '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Show/hide
|
||||||
|
if (isHidden && show) {
|
||||||
|
setIsHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
disablePrimaryButton={disableSave}
|
||||||
|
onHandledPrimaryAction={onSave}
|
||||||
|
onHide={hide}
|
||||||
|
primaryButtonName={isEditMode ? t('Save') : t('Add')}
|
||||||
|
show={show}
|
||||||
|
width="55%"
|
||||||
|
title={
|
||||||
|
<h4 data-test="annotation-layer-modal-title">
|
||||||
|
{isEditMode ? (
|
||||||
|
<StyledIcon name="edit-alt" />
|
||||||
|
) : (
|
||||||
|
<StyledIcon name="plus-large" />
|
||||||
|
)}
|
||||||
|
{isEditMode
|
||||||
|
? t('Edit Annotation Layer Properties')
|
||||||
|
: t('Add Annotation Layer')}
|
||||||
|
</h4>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledAnnotationLayerTitle>
|
||||||
|
<h4>{t('Basic Information')}</h4>
|
||||||
|
</StyledAnnotationLayerTitle>
|
||||||
|
<LayerContainer>
|
||||||
|
<div className="control-label">
|
||||||
|
{t('annotation layer name')}
|
||||||
|
<span className="required">*</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
onChange={onTextChange}
|
||||||
|
type="text"
|
||||||
|
value={currentLayer?.name}
|
||||||
|
/>
|
||||||
|
</LayerContainer>
|
||||||
|
<LayerContainer>
|
||||||
|
<div className="control-label">{t('description')}</div>
|
||||||
|
<textarea
|
||||||
|
name="descr"
|
||||||
|
value={currentLayer?.descr}
|
||||||
|
placeholder={t('Description (this can be seen in the list)')}
|
||||||
|
onChange={onTextChange}
|
||||||
|
/>
|
||||||
|
</LayerContainer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withToasts(AnnotationLayerModal);
|
@ -17,18 +17,19 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
// import React, { useMemo, useState } from 'react';
|
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
|
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
import { IconName } from 'src/components/Icon';
|
import { IconName } from 'src/components/Icon';
|
||||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
||||||
// import ListView, { Filters } from 'src/components/ListView';
|
import ListView, { Filters } from 'src/components/ListView';
|
||||||
import ListView from 'src/components/ListView';
|
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
|
import AnnotationLayerModal from './AnnotationLayerModal';
|
||||||
|
import { AnnotationLayerObject } from './types';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const MOMENT_FORMAT = 'MMM DD, YYYY';
|
const MOMENT_FORMAT = 'MMM DD, YYYY';
|
||||||
@ -38,23 +39,6 @@ interface AnnotationLayersListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to separate types file
|
|
||||||
type CreatedByUser = {
|
|
||||||
id: number;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AnnotationLayerObject = {
|
|
||||||
id?: number;
|
|
||||||
changed_on_delta_humanized?: string;
|
|
||||||
created_on?: string;
|
|
||||||
created_by?: CreatedByUser;
|
|
||||||
changed_by?: CreatedByUser;
|
|
||||||
name?: string;
|
|
||||||
desc?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AnnotationLayersList({
|
function AnnotationLayersList({
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
@ -63,29 +47,28 @@ function AnnotationLayersList({
|
|||||||
state: { loading, resourceCount: layersCount, resourceCollection: layers },
|
state: { loading, resourceCount: layersCount, resourceCollection: layers },
|
||||||
hasPerm,
|
hasPerm,
|
||||||
fetchData,
|
fetchData,
|
||||||
// refreshData,
|
refreshData,
|
||||||
} = useListViewResource<AnnotationLayerObject>(
|
} = useListViewResource<AnnotationLayerObject>(
|
||||||
'annotation_layer',
|
'annotation_layer',
|
||||||
t('annotation layers'),
|
t('annotation layers'),
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: un-comment all instances when modal work begins
|
const [annotationLayerModalOpen, setAnnotationLayerModalOpen] = useState<
|
||||||
/* const [annotationLayerModalOpen, setAnnotationLayerModalOpen] = useState<
|
|
||||||
boolean
|
boolean
|
||||||
>(false);
|
>(false);
|
||||||
const [
|
const [
|
||||||
currentAnnotationLayer,
|
currentAnnotationLayer,
|
||||||
setCurrentAnnotationLayer,
|
setCurrentAnnotationLayer,
|
||||||
] = useState<AnnotationLayerObject | null>(null); */
|
] = useState<AnnotationLayerObject | null>(null);
|
||||||
|
|
||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
|
|
||||||
function handleAnnotationLayerEdit(layer: AnnotationLayerObject | null) {
|
function handleAnnotationLayerEdit(layer: AnnotationLayerObject | null) {
|
||||||
// setCurrentAnnotationLayer(layer);
|
setCurrentAnnotationLayer(layer);
|
||||||
// setAnnotationLayerModalOpen(true);
|
setAnnotationLayerModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialSort = [{ id: 'name', desc: true }];
|
const initialSort = [{ id: 'name', desc: true }];
|
||||||
@ -215,6 +198,36 @@ function AnnotationLayersList({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filters: Filters = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: t('Created By'),
|
||||||
|
id: 'created_by',
|
||||||
|
input: 'select',
|
||||||
|
operator: 'rel_o_m',
|
||||||
|
unfilteredLabel: 'All',
|
||||||
|
fetchSelects: createFetchRelated(
|
||||||
|
'annotation_layer',
|
||||||
|
'created_by',
|
||||||
|
createErrorHandler(errMsg =>
|
||||||
|
t(
|
||||||
|
'An error occurred while fetching dataset datasource values: %s',
|
||||||
|
errMsg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
paginate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: t('Search'),
|
||||||
|
id: 'name',
|
||||||
|
input: 'search',
|
||||||
|
operator: 'ct',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const EmptyStateButton = (
|
const EmptyStateButton = (
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
@ -236,13 +249,20 @@ function AnnotationLayersList({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubMenu name={t('Annotation Layers')} buttons={subMenuButtons} />
|
<SubMenu name={t('Annotation Layers')} buttons={subMenuButtons} />
|
||||||
|
<AnnotationLayerModal
|
||||||
|
addDangerToast={addDangerToast}
|
||||||
|
layer={currentAnnotationLayer}
|
||||||
|
onLayerAdd={() => refreshData()}
|
||||||
|
onHide={() => setAnnotationLayerModalOpen(false)}
|
||||||
|
show={annotationLayerModalOpen}
|
||||||
|
/>
|
||||||
<ListView<AnnotationLayerObject>
|
<ListView<AnnotationLayerObject>
|
||||||
className="annotation-layers-list-view"
|
className="annotation-layers-list-view"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
count={layersCount}
|
count={layersCount}
|
||||||
data={layers}
|
data={layers}
|
||||||
fetchData={fetchData}
|
fetchData={fetchData}
|
||||||
// filters={filters}
|
filters={filters}
|
||||||
initialSort={initialSort}
|
initialSort={initialSort}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
|
33
superset-frontend/src/views/CRUD/annotationlayers/types.ts
Normal file
33
superset-frontend/src/views/CRUD/annotationlayers/types.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
type CreatedByUser = {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnnotationLayerObject = {
|
||||||
|
id?: number;
|
||||||
|
changed_on_delta_humanized?: string;
|
||||||
|
created_on?: string;
|
||||||
|
created_by?: CreatedByUser;
|
||||||
|
changed_by?: CreatedByUser;
|
||||||
|
name: string;
|
||||||
|
descr: string;
|
||||||
|
};
|
@ -57,6 +57,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||||||
datamodel = SQLAInterface(AnnotationLayer)
|
datamodel = SQLAInterface(AnnotationLayer)
|
||||||
|
|
||||||
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
|
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
|
||||||
|
RouteMethod.RELATED,
|
||||||
"bulk_delete", # not using RouteMethod since locally defined
|
"bulk_delete", # not using RouteMethod since locally defined
|
||||||
}
|
}
|
||||||
class_permission_name = "AnnotationLayerModelView"
|
class_permission_name = "AnnotationLayerModelView"
|
||||||
@ -64,10 +65,12 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||||||
allow_browser_login = True
|
allow_browser_login = True
|
||||||
|
|
||||||
show_columns = [
|
show_columns = [
|
||||||
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"descr",
|
"descr",
|
||||||
]
|
]
|
||||||
list_columns = [
|
list_columns = [
|
||||||
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"descr",
|
"descr",
|
||||||
"created_by.first_name",
|
"created_by.first_name",
|
||||||
@ -94,6 +97,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||||||
]
|
]
|
||||||
|
|
||||||
search_filters = {"name": [AnnotationLayerAllTextFilter]}
|
search_filters = {"name": [AnnotationLayerAllTextFilter]}
|
||||||
|
allowed_rel_fields = {"created_by"}
|
||||||
|
|
||||||
apispec_parameter_schemas = {
|
apispec_parameter_schemas = {
|
||||||
"get_delete_ids_schema": get_delete_ids_schema,
|
"get_delete_ids_schema": get_delete_ids_schema,
|
||||||
|
@ -125,6 +125,7 @@ class TestAnnotationLayerApi(SupersetTestCase):
|
|||||||
assert rv.status_code == 200
|
assert rv.status_code == 200
|
||||||
|
|
||||||
expected_result = {
|
expected_result = {
|
||||||
|
"id": annotation_layer.id,
|
||||||
"name": "name1",
|
"name": "name1",
|
||||||
"descr": "descr1",
|
"descr": "descr1",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user