mirror of
https://github.com/apache/superset.git
synced 2024-09-06 05:47:43 -04:00
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 DatasetList from 'src/views/CRUD/data/dataset/DatasetList';
|
||||||
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
|
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
|
||||||
import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList';
|
import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList';
|
||||||
|
import CssTemplatesList from 'src/views/CRUD/csstemplates/CssTemplatesList';
|
||||||
|
|
||||||
import messageToastReducer from '../messageToasts/reducers';
|
import messageToastReducer from '../messageToasts/reducers';
|
||||||
import { initEnhancer } from '../reduxUtils';
|
import { initEnhancer } from '../reduxUtils';
|
||||||
@ -97,6 +98,11 @@ const App = () => (
|
|||||||
<SavedQueryList user={user} />
|
<SavedQueryList user={user} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/csstemplatemodelview/list/">
|
||||||
|
<ErrorBoundary>
|
||||||
|
<CssTemplatesList user={user} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
<ToastPresenter />
|
<ToastPresenter />
|
||||||
</QueryParamProvider>
|
</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
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from flask_appbuilder.api import expose
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
|
from flask_appbuilder.security.decorators import has_access
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
|
|
||||||
|
from superset import app
|
||||||
from superset.constants import RouteMethod
|
from superset.constants import RouteMethod
|
||||||
|
from superset.extensions import feature_flag_manager
|
||||||
from superset.models import core as models
|
from superset.models import core as models
|
||||||
|
from superset.typing import FlaskResponse
|
||||||
from superset.views.base import DeleteMixin, SupersetModelView
|
from superset.views.base import DeleteMixin, SupersetModelView
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +43,17 @@ class CssTemplateModelView( # pylint: disable=too-many-ancestors
|
|||||||
add_columns = edit_columns
|
add_columns = edit_columns
|
||||||
label_columns = {"template_name": _("Template Name")}
|
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
|
class CssTemplateAsyncModelView( # pylint: disable=too-many-ancestors
|
||||||
CssTemplateModelView
|
CssTemplateModelView
|
||||||
|
Loading…
Reference in New Issue
Block a user