diff --git a/superset-frontend/spec/javascripts/views/CRUD/csstemplates/CssTemplatesList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/csstemplates/CssTemplatesList_spec.jsx new file mode 100644 index 0000000000..be2b05ea50 --- /dev/null +++ b/superset-frontend/spec/javascripts/views/CRUD/csstemplates/CssTemplatesList_spec.jsx @@ -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(, { 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(); + }); +}); diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx index eff9883578..4985a5bca3 100644 --- a/superset-frontend/src/views/App.tsx +++ b/superset-frontend/src/views/App.tsx @@ -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 = () => ( + + + + + diff --git a/superset-frontend/src/views/CRUD/csstemplates/CssTemplatesList.tsx b/superset-frontend/src/views/CRUD/csstemplates/CssTemplatesList.tsx new file mode 100644 index 0000000000..5793a2262a --- /dev/null +++ b/superset-frontend/src/views/CRUD/csstemplates/CssTemplatesList.tsx @@ -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( + '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 ; + }, + Header: t('Actions'), + id: 'actions', + disableSortBy: true, + size: 'xl', + }, + ], + [canDelete, canCreate], + ); + + return ( + <> + + + 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); diff --git a/superset/views/css_templates.py b/superset/views/css_templates.py index 0dff43c17a..3bd978e37a 100644 --- a/superset/views/css_templates.py +++ b/superset/views/css_templates.py @@ -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