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