[dashboard] New, list view (react) (#8845)

* adds dashboard listview component

* use new api

* use json over rison

* lint

* adds seperate dashboard list view

* edit and delete actions

* fix lint ignore

* fix common_bootstrap_payload is now a function

* fix license

* fix pylint

* isort

* fix tests

* lint

* lint ts

* fix js tests

* fix double import from bad rebase

* fix indent error

* lookup permissions

* generic permission lookup

* get tslint to pass

* adds js specs

* lint

* fix rebase

* lint

* lint again

* fix type errors preventing build

* adds more specs

* fix tslint error

* fix null check

* remove unecessary code

* use translations provided by api

* more translations

* linting

* fix spec

* i18n

* fix register order
This commit is contained in:
ʈᵃᵢ
2020-01-15 21:48:55 -08:00
committed by Maxime Beauchemin
parent 7e6719050b
commit 7b97764dbc
27 changed files with 1979 additions and 494 deletions

View File

@@ -0,0 +1,175 @@
/**
* 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 { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { MenuItem, Pagination } from 'react-bootstrap';
import ListView from 'src/components/ListView/ListView';
describe('ListView', () => {
const mockedProps = {
title: 'Data Table',
columns: [
{
accessor: 'id',
Header: 'ID',
sortable: true,
},
{
accessor: 'name',
Header: 'Name',
filterable: true,
},
],
data: [
{ id: 1, name: 'data 1' },
{ id: 2, name: 'data 2' },
],
count: 2,
pageSize: 1,
fetchData: jest.fn(() => []),
loading: false,
filterTypes: {
id: [],
name: [{ name: 'sw', label: 'Starts With' }],
},
};
const wrapper = mount(<ListView {...mockedProps} />);
afterEach(() => {
mockedProps.fetchData.mockClear();
});
it('calls fetchData on mount', () => {
expect(wrapper.find(ListView)).toHaveLength(1);
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"filters": Object {},
"pageIndex": 0,
"pageSize": 1,
"sortBy": Array [],
},
]
`);
});
it('calls fetchData on sort', () => {
wrapper
.find('[data-test="sort-header"]')
.first()
.simulate('click');
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"filters": Object {},
"pageIndex": 0,
"pageSize": 1,
"sortBy": Array [
Object {
"desc": false,
"id": "id",
},
],
},
]
`);
});
it('calls fetchData on filter', () => {
act(() => {
wrapper
.find('.dropdown-toggle')
.children('button')
.props()
.onClick();
wrapper
.find(MenuItem)
.props()
.onSelect({ id: 'name', Header: 'name' });
});
wrapper.update();
act(() => {
wrapper.find('.filter-inputs input[type="text"]').prop('onChange')({
currentTarget: { value: 'foo' },
});
});
wrapper.update();
act(() => {
wrapper
.find('[data-test="apply-filters"]')
.last()
.prop('onClick')();
});
wrapper.update();
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"filters": Object {
"name": Object {
"filterId": "sw",
"filterValue": "foo",
},
},
"pageIndex": 0,
"pageSize": 1,
"sortBy": Array [
Object {
"desc": false,
"id": "id",
},
],
},
]
`);
});
it('calls fetchData on page change', () => {
act(() => {
wrapper.find(Pagination).prop('onSelect')(2);
});
wrapper.update();
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"filters": Object {
"name": Object {
"filterId": "sw",
"filterValue": "foo",
},
},
"pageIndex": 1,
"pageSize": 1,
"sortBy": Array [
Object {
"desc": false,
"id": "id",
},
],
},
]
`);
});
});

View File

@@ -68,7 +68,7 @@ describe('ExploreChartHeader', () => {
it('should updateChartTitleOrSaveSlice for existed slice', () => {
const newTitle = 'New Chart Title';
wrapper.instance().updateChartTitleOrSaveSlice(newTitle);
expect(stub.call.length).toEqual(1);
expect(stub.call).toHaveLength(1);
expect(stub).toHaveBeenCalledWith(mockProps.slice.form_data, {
action: 'overwrite',
slice_name: newTitle,
@@ -79,7 +79,7 @@ describe('ExploreChartHeader', () => {
const newTitle = 'New Chart Title';
wrapper.setProps({ slice: undefined });
wrapper.instance().updateChartTitleOrSaveSlice(newTitle);
expect(stub.call.length).toEqual(1);
expect(stub.call).toHaveLength(1);
expect(stub).toHaveBeenCalledWith(mockProps.form_data, {
action: 'saveas',
slice_name: newTitle,

View File

@@ -0,0 +1,82 @@
/**
* 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 { mount } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import DashboardList from 'src/views/dashboardList/DashboardList';
import ListView from 'src/components/ListView/ListView';
// store needed for withToasts(DashboardTable)
const mockStore = configureStore([thunk]);
const store = mockStore({});
const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
const mockDashboards = [...new Array(3)].map((_, i) => ({
id: i,
url: 'url',
dashboard_title: `title ${i}`,
changed_by_name: 'user',
changed_by_url: 'changed_by_url',
changed_by_fk: 1,
published: true,
changed_on: new Date().toISOString(),
}));
fetchMock.get(dashboardsInfoEndpoint, {
permissions: ['can_list', 'can_edit'],
filters: [],
});
fetchMock.get(dashboardsEndpoint, {
result: mockDashboards,
dashboard_count: 3,
});
describe('DashboardList', () => {
const mockedProps = {};
const wrapper = mount(<DashboardList {...mockedProps} />, {
context: { store },
});
it('renders', () => {
expect(wrapper.find(DashboardList)).toHaveLength(1);
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toHaveLength(1);
});
it('fetches info', () => {
const callsI = fetchMock.calls(/dashboard\/_info/);
expect(callsI).toHaveLength(1);
});
it('fetches data', () => {
wrapper.update();
const callsD = fetchMock.calls(/dashboard\/\?q/);
expect(callsD).toHaveLength(1);
expect(callsD[0][0]).toMatchInlineSnapshot(
`"/http//localhost/api/v1/dashboard/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`,
);
});
});

View File

@@ -21,16 +21,16 @@ import { mount } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { Table } from 'reactable-arc';
import ListView from 'src/components/ListView/ListView';
import DashboardTable from '../../../src/welcome/DashboardTable';
import Loading from '../../../src/components/Loading';
// store needed for withToasts(TableLoader)
// store needed for withToasts(DashboardTable)
const mockStore = configureStore([thunk]);
const store = mockStore({});
const dashboardsEndpoint = 'glob:*/dashboardasync/api/read*';
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*';
const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }];
fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
@@ -48,7 +48,7 @@ describe('DashboardTable', () => {
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('fetches dashboards and renders a Table', done => {
it('fetches dashboards and renders a ListView', done => {
const wrapper = setup();
setTimeout(() => {
@@ -56,7 +56,7 @@ describe('DashboardTable', () => {
// there's a delay between response and updating state, so manually set it
// rather than adding a timeout which could introduce flakiness
wrapper.setState({ dashaboards: mockDashboards });
expect(wrapper.find(Table)).toHaveLength(1);
expect(wrapper.find(ListView)).toHaveLength(1);
done();
});
});