mirror of https://github.com/apache/superset.git
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 AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList';
|
||||
import AnnotationLayerModal from 'src/views/CRUD/annotationlayers/AnnotationLayerModal';
|
||||
import SubMenu from 'src/components/Menu/SubMenu';
|
||||
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 Button from 'src/components/Button';
|
||||
// import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||
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)
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
@ -39,7 +40,7 @@ const store = mockStore({});
|
|||
const layersInfoEndpoint = 'glob:*/api/v1/annotation_layer/_info*';
|
||||
const layersEndpoint = '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) => ({
|
||||
changed_on_delta_humanized: `${i} day(s) ago`,
|
||||
|
@ -63,14 +64,14 @@ fetchMock.get(layersEndpoint, {
|
|||
});
|
||||
|
||||
/* fetchMock.delete(layerEndpoint, {});
|
||||
fetchMock.delete(layersEndpoint, {});
|
||||
fetchMock.delete(layersEndpoint, {}); */
|
||||
|
||||
fetchMock.get(layersRelatedEndpoint, {
|
||||
created_by: {
|
||||
count: 0,
|
||||
result: [],
|
||||
},
|
||||
}); */
|
||||
});
|
||||
|
||||
describe('AnnotationLayersList', () => {
|
||||
const wrapper = mount(<AnnotationLayersList />, { context: { store } });
|
||||
|
@ -91,6 +92,10 @@ describe('AnnotationLayersList', () => {
|
|||
expect(wrapper.find(ListView)).toExist();
|
||||
});
|
||||
|
||||
it('renders a modal', () => {
|
||||
expect(wrapper.find(AnnotationLayerModal)).toExist();
|
||||
});
|
||||
|
||||
it('fetches layers', () => {
|
||||
const callsQ = fetchMock.calls(/annotation_layer\/\?q/);
|
||||
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)"`,
|
||||
);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
const ListViewStyles = styled.div`
|
||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
text-align: center;
|
||||
|
||||
.superset-list-view {
|
||||
|
@ -58,6 +57,7 @@ const ListViewStyles = styled.div`
|
|||
}
|
||||
}
|
||||
.body {
|
||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
}
|
||||
|
||||
.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.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
// import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import moment from 'moment';
|
||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||
import { IconName } from 'src/components/Icon';
|
||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
||||
// import ListView, { Filters } from 'src/components/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import ListView, { Filters } from 'src/components/ListView';
|
||||
import Button from 'src/components/Button';
|
||||
import AnnotationLayerModal from './AnnotationLayerModal';
|
||||
import { AnnotationLayerObject } from './types';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const MOMENT_FORMAT = 'MMM DD, YYYY';
|
||||
|
@ -38,23 +39,6 @@ interface AnnotationLayersListProps {
|
|||
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({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
|
@ -63,29 +47,28 @@ function AnnotationLayersList({
|
|||
state: { loading, resourceCount: layersCount, resourceCollection: layers },
|
||||
hasPerm,
|
||||
fetchData,
|
||||
// refreshData,
|
||||
refreshData,
|
||||
} = useListViewResource<AnnotationLayerObject>(
|
||||
'annotation_layer',
|
||||
t('annotation layers'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
// TODO: un-comment all instances when modal work begins
|
||||
/* const [annotationLayerModalOpen, setAnnotationLayerModalOpen] = useState<
|
||||
const [annotationLayerModalOpen, setAnnotationLayerModalOpen] = useState<
|
||||
boolean
|
||||
>(false);
|
||||
const [
|
||||
currentAnnotationLayer,
|
||||
setCurrentAnnotationLayer,
|
||||
] = useState<AnnotationLayerObject | null>(null); */
|
||||
] = useState<AnnotationLayerObject | null>(null);
|
||||
|
||||
const canCreate = hasPerm('can_add');
|
||||
const canEdit = hasPerm('can_edit');
|
||||
const canDelete = hasPerm('can_delete');
|
||||
|
||||
function handleAnnotationLayerEdit(layer: AnnotationLayerObject | null) {
|
||||
// setCurrentAnnotationLayer(layer);
|
||||
// setAnnotationLayerModalOpen(true);
|
||||
setCurrentAnnotationLayer(layer);
|
||||
setAnnotationLayerModalOpen(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 = (
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
|
@ -236,13 +249,20 @@ function AnnotationLayersList({
|
|||
return (
|
||||
<>
|
||||
<SubMenu name={t('Annotation Layers')} buttons={subMenuButtons} />
|
||||
<AnnotationLayerModal
|
||||
addDangerToast={addDangerToast}
|
||||
layer={currentAnnotationLayer}
|
||||
onLayerAdd={() => refreshData()}
|
||||
onHide={() => setAnnotationLayerModalOpen(false)}
|
||||
show={annotationLayerModalOpen}
|
||||
/>
|
||||
<ListView<AnnotationLayerObject>
|
||||
className="annotation-layers-list-view"
|
||||
columns={columns}
|
||||
count={layersCount}
|
||||
data={layers}
|
||||
fetchData={fetchData}
|
||||
// filters={filters}
|
||||
filters={filters}
|
||||
initialSort={initialSort}
|
||||
loading={loading}
|
||||
pageSize={PAGE_SIZE}
|
||||
|
|
|
@ -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)
|
||||
|
||||
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
|
||||
RouteMethod.RELATED,
|
||||
"bulk_delete", # not using RouteMethod since locally defined
|
||||
}
|
||||
class_permission_name = "AnnotationLayerModelView"
|
||||
|
@ -64,10 +65,12 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
allow_browser_login = True
|
||||
|
||||
show_columns = [
|
||||
"id",
|
||||
"name",
|
||||
"descr",
|
||||
]
|
||||
list_columns = [
|
||||
"id",
|
||||
"name",
|
||||
"descr",
|
||||
"created_by.first_name",
|
||||
|
@ -94,6 +97,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
]
|
||||
|
||||
search_filters = {"name": [AnnotationLayerAllTextFilter]}
|
||||
allowed_rel_fields = {"created_by"}
|
||||
|
||||
apispec_parameter_schemas = {
|
||||
"get_delete_ids_schema": get_delete_ids_schema,
|
||||
|
|
|
@ -125,6 +125,7 @@ class TestAnnotationLayerApi(SupersetTestCase):
|
|||
assert rv.status_code == 200
|
||||
|
||||
expected_result = {
|
||||
"id": annotation_layer.id,
|
||||
"name": "name1",
|
||||
"descr": "descr1",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue