mirror of https://github.com/apache/superset.git
feat: CSS Templates List (#11189)
This commit is contained in:
parent
7b0dabd7aa
commit
a6fc3d2384
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue