You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/10/09 23:32:54 UTC

[incubator-superset] branch master updated: feat: CSS Templates List (#11189)

This is an automated email from the ASF dual-hosted git repository.

tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new a6fc3d2  feat: CSS Templates List (#11189)
a6fc3d2 is described below

commit a6fc3d2384627c59a02bc310a7346fe9914d4332
Author: Moriah Kreeger <mo...@gmail.com>
AuthorDate: Fri Oct 9 16:32:31 2020 -0700

    feat: CSS Templates List (#11189)
---
 .../CRUD/csstemplates/CssTemplatesList_spec.jsx    |  77 +++++++++
 superset-frontend/src/views/App.tsx                |   6 +
 .../views/CRUD/csstemplates/CssTemplatesList.tsx   | 187 +++++++++++++++++++++
 superset/views/css_templates.py                    |  16 ++
 4 files changed, 286 insertions(+)

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 0000000..be2b05e
--- /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(<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();
+  });
+});
diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx
index eff9883..4985a5b 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 = () => (
                   <SavedQueryList user={user} />
                 </ErrorBoundary>
               </Route>
+              <Route path="/csstemplatemodelview/list/">
+                <ErrorBoundary>
+                  <CssTemplatesList user={user} />
+                </ErrorBoundary>
+              </Route>
             </Switch>
             <ToastPresenter />
           </QueryParamProvider>
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 0000000..5793a22
--- /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<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);
diff --git a/superset/views/css_templates.py b/superset/views/css_templates.py
index 0dff43c..3bd978e 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