feat: CSS Templates List (#11189)

This commit is contained in:
Moriah Kreeger 2020-10-09 16:32:31 -07:00 committed by GitHub
parent 7b0dabd7aa
commit a6fc3d2384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 286 additions and 0 deletions

View File

@ -0,0 +1,77 @@
/**
* 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 { styledMount as mount } from 'spec/helpers/theming';
import CssTemplatesList from 'src/views/CRUD/csstemplates/CssTemplatesList';
import SubMenu from 'src/components/Menu/SubMenu';
import ListView from 'src/components/ListView';
// import Filters from 'src/components/ListView/Filters';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
// import { act } from 'react-dom/test-utils';
// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({});
const templatesInfoEndpoint = 'glob:*/api/v1/css_template/_info*';
const templatesEndpoint = 'glob:*/api/v1/css_template/?*';
const mocktemplates = [...new Array(3)].map((_, i) => ({
changed_on_delta_humanized: `${i} day(s) ago`,
created_by: {
first_name: `user`,
last_name: `${i}`,
},
created_on: new Date().toISOString,
css: 'css',
id: i,
template_name: `template ${i}`,
}));
fetchMock.get(templatesInfoEndpoint, {
permissions: ['can_delete'],
});
fetchMock.get(templatesEndpoint, {
result: mocktemplates,
templates_count: 3,
});
describe('CssTemplatesList', () => {
const wrapper = mount(<CssTemplatesList />, { context: { store } });
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
});
it('renders', () => {
expect(wrapper.find(CssTemplatesList)).toExist();
});
it('renders a SubMenu', () => {
expect(wrapper.find(SubMenu)).toExist();
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toExist();
});
});

View File

@ -33,6 +33,7 @@ import ChartList from 'src/views/CRUD/chart/ChartList';
import DatasetList from 'src/views/CRUD/data/dataset/DatasetList';
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList';
import CssTemplatesList from 'src/views/CRUD/csstemplates/CssTemplatesList';
import messageToastReducer from '../messageToasts/reducers';
import { initEnhancer } from '../reduxUtils';
@ -97,6 +98,11 @@ const App = () => (
<SavedQueryList user={user} />
</ErrorBoundary>
</Route>
<Route path="/csstemplatemodelview/list/">
<ErrorBoundary>
<CssTemplatesList user={user} />
</ErrorBoundary>
</Route>
</Switch>
<ToastPresenter />
</QueryParamProvider>

View File

@ -0,0 +1,187 @@
/**
* 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, { useMemo } from 'react';
import { t } from '@superset-ui/core';
import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import SubMenu 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';
const PAGE_SIZE = 25;
interface CssTemplatesListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
}
type TemplateObject = {
id?: number;
changed_on_delta_humanized: string;
created_on: string;
created_by: {
id: number;
first_name: string;
last_name: string;
};
css: string;
template_name: string;
};
function CssTemplatesList({
addDangerToast,
addSuccessToast,
}: CssTemplatesListProps) {
const {
state: {
loading,
resourceCount: templatesCount,
resourceCollection: templates,
},
hasPerm,
fetchData,
// refreshData,
} = useListViewResource<TemplateObject>(
'css_template',
t('css templates'),
addDangerToast,
);
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
const initialSort = [{ id: 'template_name', desc: true }];
const columns = useMemo(
() => [
{
accessor: 'template_name',
Header: t('Name'),
},
{
Cell: ({
row: {
original: { created_on: createdOn },
},
}: any) => {
const date = new Date(createdOn);
const utc = new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
),
);
return moment(utc).fromNow();
},
Header: t('Created On'),
accessor: 'created_on',
size: 'xl',
},
{
accessor: 'created_by',
disableSortBy: true,
Header: t('Created By'),
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => changedOn,
Header: t('Last Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => {}; // handleDatabaseEdit(original);
const handleDelete = () => {}; // openDatabaseDeleteModal(original);
const actions = [
canEdit
? {
label: 'edit-action',
tooltip: t('Edit template'),
placement: 'bottom',
icon: 'edit' as IconName,
onClick: handleEdit,
}
: null,
canDelete
? {
label: 'delete-action',
tooltip: t('Delete template'),
placement: 'bottom',
icon: 'trash' as IconName,
onClick: handleDelete,
}
: null,
].filter(item => !!item);
if (!canEdit && !canDelete) {
return null;
}
return <ActionsBar actions={actions as ActionProps[]} />;
},
Header: t('Actions'),
id: 'actions',
disableSortBy: true,
size: 'xl',
},
],
[canDelete, canCreate],
);
return (
<>
<SubMenu name={t('CSS Templates')} />
<ListView<TemplateObject>
className="css-templates-list-view"
columns={columns}
count={templatesCount}
data={templates}
fetchData={fetchData}
// filters={filters}
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
/>
</>
);
}
export default withToasts(CssTemplatesList);

View File

@ -14,11 +14,16 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask_appbuilder.api import expose
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import lazy_gettext as _
from superset import app
from superset.constants import RouteMethod
from superset.extensions import feature_flag_manager
from superset.models import core as models
from superset.typing import FlaskResponse
from superset.views.base import DeleteMixin, SupersetModelView
@ -38,6 +43,17 @@ class CssTemplateModelView( # pylint: disable=too-many-ancestors
add_columns = edit_columns
label_columns = {"template_name": _("Template Name")}
@expose("/list/")
@has_access
def list(self) -> FlaskResponse:
if not (
app.config["ENABLE_REACT_CRUD_VIEWS"]
and feature_flag_manager.is_feature_enabled("SIP_34_CSS_TEMPLATES_UI")
):
return super().list()
return super().render_app_template()
class CssTemplateAsyncModelView( # pylint: disable=too-many-ancestors
CssTemplateModelView