You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2025/04/01 07:45:16 UTC

(superset) branch template_less_preset_theme_editor created (now a2c8621826)

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

maximebeauchemin pushed a change to branch template_less_preset_theme_editor
in repository https://gitbox.apache.org/repos/asf/superset.git


      at a2c8621826 all working

This branch includes the following new commits:

     new 07a29c7aeb A fancy ThemeEditor for Preset
     new e18d5201d0 apply theme now works
     new a2c8621826 all working

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 03/03: all working

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch template_less_preset_theme_editor
in repository https://gitbox.apache.org/repos/asf/superset.git

commit a2c8621826ce35c294fd3cad490e969421de3970
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Tue Apr 1 00:42:43 2025 -0700

    all working
---
 superset-frontend/src/components/ThemeEditor/index.tsx | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/src/components/ThemeEditor/index.tsx b/superset-frontend/src/components/ThemeEditor/index.tsx
index 85708ab41f..f1337b1fb8 100644
--- a/superset-frontend/src/components/ThemeEditor/index.tsx
+++ b/superset-frontend/src/components/ThemeEditor/index.tsx
@@ -97,7 +97,7 @@ export default function ThemeEditor() {
   };
 
   const updateColorBgBase = dark => {
-    setToken('colorBgBase', dark ? '#141414' : '#ffffff');
+    setToken('colorBgBase', dark ? '#000' : '#fff');
   };
 
   const getMergedTheme = () => {
@@ -105,21 +105,22 @@ export default function ThemeEditor() {
     try {
       overrides = JSON.parse(jsonOverrides);
     } catch (e) {
-      console.log('Invalid JSON in overrides:', e);
+      console.error('Invalid JSON in overrides:', e);
     }
     const algorithm = [
       isDark ? 'dark' : 'default',
       ...(isCompact ? ['compact'] : []),
     ];
     return {
-      tokens: { ...tokens, ...overrides },
+      token: { ...tokens, ...overrides },
       algorithm,
     };
   };
 
   const applyTheme = () => {
     try {
-      themeObject.setConfig(getMergedTheme());
+      const antdTheme = getMergedTheme();
+      themeObject.setConfig(antdTheme);
     } catch (e) {
       console.error('Failed to apply theme overrides:', e);
     }
@@ -254,7 +255,6 @@ function ThemeToken({ token, type, tokens, setToken }) {
             {...commonProps}
             value={value}
             onChange={(_, hex) => handleChange(hex)}
-            format="hex"
           />
         );
       case 'number':


(superset) 01/03: A fancy ThemeEditor for Preset

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch template_less_preset_theme_editor
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 07a29c7aebcd0fde7ec8e775d1b8725b66326579
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Sun Mar 30 21:49:06 2025 -0700

    A fancy ThemeEditor for Preset
---
 .../src/components/ThemeEditor/index.tsx           | 334 ++++++++++++++-------
 1 file changed, 232 insertions(+), 102 deletions(-)

diff --git a/superset-frontend/src/components/ThemeEditor/index.tsx b/superset-frontend/src/components/ThemeEditor/index.tsx
index 2c819393d1..cba265c10a 100644
--- a/superset-frontend/src/components/ThemeEditor/index.tsx
+++ b/superset-frontend/src/components/ThemeEditor/index.tsx
@@ -3,7 +3,7 @@
  * 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
+ * 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
  *
@@ -16,132 +16,262 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Modal, Tooltip, Flex, Select } from 'antd-v5';
-import Button from 'src/components/Button';
-import {
-  themeObject,
-  exampleThemes,
-  SerializableThemeConfig,
-  SupersetTheme,
-} from '@superset-ui/core';
 import { useState } from 'react';
+import {
+  Modal,
+  Tooltip,
+  Card,
+  Input,
+  InputNumber,
+  Typography,
+  Form,
+  Collapse,
+  Switch,
+  Checkbox,
+  ColorPicker,
+} from 'antd-v5';
+import Button from 'src/components/Button';
 import { Icons } from 'src/components/Icons';
 import { JsonEditor } from 'src/components/AsyncAceEditor';
+import { themeObject, t } from '@superset-ui/core';
+import { mergeWith } from 'lodash';
 
-interface ThemeEditorProps {
-  initialTheme?: SupersetTheme;
-  tooltipTitle?: string;
-  modalTitle?: string;
-}
-
-const ThemeEditor: React.FC<ThemeEditorProps> = ({
-  initialTheme = {},
-  tooltipTitle = 'Edit Theme',
-  modalTitle = 'Theme Editor',
-}) => {
-  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
-  const jsonTheme: string = themeObject.json();
-  const [jsonMetadata, setJsonMetadata] = useState<string>(jsonTheme);
-  const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
+const { Title } = Typography;
+const { Panel } = Collapse;
 
-  // Get theme names for the Select options
-  const themeOptions: { value: string; label: string }[] = Object.keys(
-    exampleThemes,
-  ).map(key => ({
-    value: key,
-    label: key,
-  }));
+// Manually curated list of seed tokens (excluding animation-related ones)
+const seedTokenCategories: Record<
+  string,
+  { token: string; type: 'color' | 'number' | 'string' }[]
+> = {
+  Colors: [
+    { token: 'colorPrimary', type: 'color' },
+    { token: 'colorSuccess', type: 'color' },
+    { token: 'colorWarning', type: 'color' },
+    { token: 'colorError', type: 'color' },
+    { token: 'colorInfo', type: 'color' },
+    { token: 'colorBgBase', type: 'color' },
+  ],
+  Typography: [
+    { token: 'fontFamily', type: 'string' },
+    { token: 'fontFamilyCode', type: 'string' },
+    { token: 'fontSize', type: 'number' },
+    { token: 'fontWeightStrong', type: 'number' },
+    { token: 'lineHeight', type: 'number' },
+  ],
+  Layout: [
+    { token: 'borderRadius', type: 'number' },
+    { token: 'sizeUnit', type: 'number' },
+    { token: 'controlHeight', type: 'number' },
+    { token: 'zIndexBase', type: 'number' },
+    { token: 'zIndexPopupBase', type: 'number' },
+  ],
+};
 
-  const handleOpenModal = (): void => {
-    setIsModalOpen(true);
-  };
+export default function ThemeEditor() {
+  const initialTheme = themeObject.toSerializedConfig();
+  const filteredKeys = Object.values(seedTokenCategories)
+    .flat()
+    .map(entry => entry.token);
+  const initialTokens = filteredKeys.reduce(
+    (acc, key) => {
+      acc[key] = themeObject.theme[key];
+      return acc;
+    },
+    {} as Record<string, any>,
+  );
 
-  const handleCancel = (): void => {
-    setIsModalOpen(false);
-  };
+  const { algorithm } = initialTheme;
+  const [tokens, setTokens] = useState<Record<string, any>>(initialTokens);
+  const [jsonOverrides, setJsonOverrides] = useState<string>('{}');
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDark, setIsDark] = useState(algorithm?.includes('dark'));
+  const [isCompact, setIsCompact] = useState(algorithm?.includes('compact'));
 
-  const handleSave = (): void => {
+  const getMergedTheme = () => {
     try {
-      const parsedTheme = JSON.parse(jsonMetadata);
-      console.log('Parsed theme:', parsedTheme);
-      themeObject.setConfig(parsedTheme);
-      setIsModalOpen(false);
-    } catch (error) {
-      console.error('Invalid JSON in theme editor:', error);
-      alert('Error parsing JSON. Please check your input.');
+      const overrides = JSON.parse(jsonOverrides);
+      const merged = mergeWith({}, tokens, overrides);
+      merged.algorithm = [
+        isDark ? 'dark' : 'default',
+        ...(isCompact ? ['compact'] : []),
+      ];
+      return merged;
+    } catch (e) {
+      return tokens;
     }
   };
 
-  const handleThemeChange = (value: string): void => {
-    setSelectedTheme(value);
-    // When a theme is selected, update the JSON editor with the theme definition
-    const themeData = exampleThemes[value] || ({} as SerializableThemeConfig);
-    setJsonMetadata(JSON.stringify(themeData, null, 2));
-  };
-
   return (
     <>
-      <Tooltip title={tooltipTitle} placement="bottom">
+      <Tooltip title={t('Edit Theme')} placement="bottom">
         <Button
           buttonStyle="link"
-          icon={
-            <Icons.BgColorsOutlined
-              iconSize="l"
-              iconColor={themeObject.theme.colorPrimary}
-            />
-          }
-          onClick={handleOpenModal}
-          aria-label="Edit theme"
+          icon={<Icons.BgColorsOutlined iconSize="l" />}
+          onClick={() => setIsModalOpen(true)}
+          aria-label={t('Edit theme')}
           size="large"
         />
       </Tooltip>
       <Modal
-        title={modalTitle}
+        title={t('Theme Editor')}
         open={isModalOpen}
-        onCancel={handleCancel}
+        onCancel={() => setIsModalOpen(false)}
+        onOk={() => setIsModalOpen(false)}
         width={800}
         centered
-        styles={{
-          body: {
-            padding: '24px',
-          },
-        }}
-        footer={
-          <Flex justify="end" gap="small">
-            <Button onClick={handleCancel} buttonStyle="secondary">
-              Cancel
-            </Button>
-            <Button type="primary" onClick={handleSave}>
-              Apply Theme
-            </Button>
-          </Flex>
-        }
       >
-        <Flex vertical gap="middle">
-          <div>
-            Select a theme template:
-            <Select
-              placeholder="Choose a theme"
-              style={{ width: '100%', marginTop: '8px' }}
-              options={themeOptions}
-              onChange={handleThemeChange}
-              value={selectedTheme}
-            />
-          </div>
-          <JsonEditor
-            showLoadingForImport
-            name="json_metadata"
-            value={jsonMetadata}
-            onChange={setJsonMetadata}
-            tabSize={2}
-            width="100%"
-            height="200px"
-            wrapEnabled
-          />
-        </Flex>
+        <Collapse defaultActiveKey={['algorithms', 'Colors']}>
+          <Panel header={t('Algorithms')} key="algorithms">
+            <Form layout="horizontal">
+              <Form.Item label={t('Dark Mode')}>
+                <Switch
+                  checked={isDark}
+                  onChange={setIsDark}
+                  checkedChildren={t('Dark')}
+                  unCheckedChildren={t('Light')}
+                />
+              </Form.Item>
+              <Form.Item label={t('Compact Mode')}>
+                <Checkbox
+                  checked={isCompact}
+                  onChange={e => setIsCompact(e.target.checked)}
+                />
+              </Form.Item>
+            </Form>
+          </Panel>
+
+          {Object.entries(seedTokenCategories).map(([section, items]) => (
+            <Panel header={t(section)} key={section}>
+              <ThemeSection
+                layout={section === 'Colors' ? 'horizontal' : 'default'}
+              >
+                {items.map(({ token, type }) => (
+                  <ThemeToken
+                    key={token}
+                    token={token}
+                    type={type}
+                    tokens={tokens}
+                    setTokens={setTokens}
+                  />
+                ))}
+              </ThemeSection>
+            </Panel>
+          ))}
+
+          <Panel header={t('Raw JSON Overrides')} key="overrides">
+            <Card bodyStyle={{ padding: 0 }}>
+              <JsonEditor
+                showLoadingForImport
+                name="json_overrides"
+                value={jsonOverrides}
+                onChange={setJsonOverrides}
+                tabSize={2}
+                width="100%"
+                height="200px"
+                wrapEnabled
+              />
+            </Card>
+          </Panel>
+
+          <Panel header={t('Resolved Theme Output')} key="resolved">
+            <Card bodyStyle={{ padding: 12 }}>
+              <pre style={{ whiteSpace: 'pre-wrap', fontSize: 12 }}>
+                {JSON.stringify(getMergedTheme(), null, 2)}
+              </pre>
+            </Card>
+          </Panel>
+        </Collapse>
       </Modal>
     </>
   );
-};
+}
+function ThemeSection({
+  children,
+  layout,
+}: {
+  children: React.ReactNode;
+  layout?: 'horizontal' | 'default';
+}) {
+  return layout === 'horizontal' ? (
+    <Form layout="horizontal">
+      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
+        {children}
+      </div>
+    </Form>
+  ) : (
+    <Form layout="vertical">
+      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
+        {children}
+      </div>
+    </Form>
+  );
+}
+
+function ThemeToken({
+  token,
+  type,
+  tokens,
+  setTokens,
+}: {
+  token: string;
+  type: 'color' | 'number' | 'string';
+  tokens: Record<string, any>;
+  setTokens: React.Dispatch<React.SetStateAction<Record<string, any>>>;
+}) {
+  const initialValue = tokens[token];
+  const [value, setValue] = useState(initialValue);
+
+  const handleChange = (val: any) => {
+    const normalized =
+      type === 'number'
+        ? typeof val === 'number' && !Number.isNaN(val)
+          ? val
+          : null
+        : val;
+    setValue(normalized);
+    setTokens(prev => ({ ...prev, [token]: normalized }));
+  };
+
+  const renderInput = () => {
+    switch (type) {
+      case 'color':
+        return (
+          <ColorPicker
+            value={value}
+            onChange={(_, hex) => handleChange(hex)}
+            format="hex"
+          />
+        );
+      case 'number':
+        return (
+          <InputNumber
+            style={{ width: '100%' }}
+            value={value}
+            onChange={handleChange}
+          />
+        );
+      case 'string':
+      default:
+        return (
+          <Input value={value} onChange={e => handleChange(e.target.value)} />
+        );
+    }
+  };
 
-export default ThemeEditor;
+  const width =
+    type === 'string'
+      ? token === 'fontFamily' || token === 'fontFamilyCode'
+        ? 280
+        : 240
+      : 160;
+
+  return (
+    <div style={{ width, marginBottom: 0 }}>
+      <label style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>
+        {token}
+      </label>
+      {renderInput()}
+    </div>
+  );
+}


(superset) 02/03: apply theme now works

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch template_less_preset_theme_editor
in repository https://gitbox.apache.org/repos/asf/superset.git

commit e18d5201d0173bb1a428ed0ea3e0b7542d5f069f
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Tue Apr 1 00:25:50 2025 -0700

    apply theme now works
---
 .../src/components/ThemeEditor/index.tsx           | 141 ++++++++++++---------
 1 file changed, 79 insertions(+), 62 deletions(-)

diff --git a/superset-frontend/src/components/ThemeEditor/index.tsx b/superset-frontend/src/components/ThemeEditor/index.tsx
index cba265c10a..85708ab41f 100644
--- a/superset-frontend/src/components/ThemeEditor/index.tsx
+++ b/superset-frontend/src/components/ThemeEditor/index.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 import {
   Modal,
   Tooltip,
@@ -39,18 +39,15 @@ import { mergeWith } from 'lodash';
 const { Title } = Typography;
 const { Panel } = Collapse;
 
-// Manually curated list of seed tokens (excluding animation-related ones)
-const seedTokenCategories: Record<
-  string,
-  { token: string; type: 'color' | 'number' | 'string' }[]
-> = {
+const seedTokenCategories = {
   Colors: [
+    { token: 'colorBgBase', type: 'color' },
     { token: 'colorPrimary', type: 'color' },
     { token: 'colorSuccess', type: 'color' },
     { token: 'colorWarning', type: 'color' },
     { token: 'colorError', type: 'color' },
     { token: 'colorInfo', type: 'color' },
-    { token: 'colorBgBase', type: 'color' },
+    { token: 'colorLink', type: 'color' },
   ],
   Typography: [
     { token: 'fontFamily', type: 'string' },
@@ -69,37 +66,64 @@ const seedTokenCategories: Record<
 };
 
 export default function ThemeEditor() {
-  const initialTheme = themeObject.toSerializedConfig();
-  const filteredKeys = Object.values(seedTokenCategories)
-    .flat()
-    .map(entry => entry.token);
-  const initialTokens = filteredKeys.reduce(
-    (acc, key) => {
+  const [tokens, setTokens] = useState({});
+  const [jsonOverrides, setJsonOverrides] = useState('{}');
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDark, setIsDark] = useState(false);
+  const [isCompact, setIsCompact] = useState(false);
+
+  useEffect(() => {
+    if (!isModalOpen) return;
+
+    const initialTheme = themeObject.toSerializedConfig();
+    const { algorithm } = initialTheme;
+
+    const filteredKeys = Object.values(seedTokenCategories)
+      .flat()
+      .map(entry => entry.token);
+
+    const initialTokens = filteredKeys.reduce((acc, key) => {
       acc[key] = themeObject.theme[key];
       return acc;
-    },
-    {} as Record<string, any>,
-  );
+    }, {});
 
-  const { algorithm } = initialTheme;
-  const [tokens, setTokens] = useState<Record<string, any>>(initialTokens);
-  const [jsonOverrides, setJsonOverrides] = useState<string>('{}');
-  const [isModalOpen, setIsModalOpen] = useState(false);
-  const [isDark, setIsDark] = useState(algorithm?.includes('dark'));
-  const [isCompact, setIsCompact] = useState(algorithm?.includes('compact'));
+    setTokens(initialTokens);
+    setIsDark(algorithm?.includes('dark'));
+    setIsCompact(algorithm?.includes('compact'));
+  }, [isModalOpen]);
+
+  const setToken = (key, value) => {
+    setTokens(prev => ({ ...prev, [key]: value }));
+  };
+
+  const updateColorBgBase = dark => {
+    setToken('colorBgBase', dark ? '#141414' : '#ffffff');
+  };
 
   const getMergedTheme = () => {
+    let overrides = {};
+    try {
+      overrides = JSON.parse(jsonOverrides);
+    } catch (e) {
+      console.log('Invalid JSON in overrides:', e);
+    }
+    const algorithm = [
+      isDark ? 'dark' : 'default',
+      ...(isCompact ? ['compact'] : []),
+    ];
+    return {
+      tokens: { ...tokens, ...overrides },
+      algorithm,
+    };
+  };
+
+  const applyTheme = () => {
     try {
-      const overrides = JSON.parse(jsonOverrides);
-      const merged = mergeWith({}, tokens, overrides);
-      merged.algorithm = [
-        isDark ? 'dark' : 'default',
-        ...(isCompact ? ['compact'] : []),
-      ];
-      return merged;
+      themeObject.setConfig(getMergedTheme());
     } catch (e) {
-      return tokens;
+      console.error('Failed to apply theme overrides:', e);
     }
+    setIsModalOpen(false);
   };
 
   return (
@@ -117,7 +141,7 @@ export default function ThemeEditor() {
         title={t('Theme Editor')}
         open={isModalOpen}
         onCancel={() => setIsModalOpen(false)}
-        onOk={() => setIsModalOpen(false)}
+        onOk={applyTheme}
         width={800}
         centered
       >
@@ -127,7 +151,10 @@ export default function ThemeEditor() {
               <Form.Item label={t('Dark Mode')}>
                 <Switch
                   checked={isDark}
-                  onChange={setIsDark}
+                  onChange={val => {
+                    setIsDark(val);
+                    updateColorBgBase(val);
+                  }}
                   checkedChildren={t('Dark')}
                   unCheckedChildren={t('Light')}
                 />
@@ -152,7 +179,7 @@ export default function ThemeEditor() {
                     token={token}
                     type={type}
                     tokens={tokens}
-                    setTokens={setTokens}
+                    setToken={setToken}
                   />
                 ))}
               </ThemeSection>
@@ -186,13 +213,8 @@ export default function ThemeEditor() {
     </>
   );
 }
-function ThemeSection({
-  children,
-  layout,
-}: {
-  children: React.ReactNode;
-  layout?: 'horizontal' | 'default';
-}) {
+
+function ThemeSection({ children, layout }) {
   return layout === 'horizontal' ? (
     <Form layout="horizontal">
       <div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
@@ -207,37 +229,29 @@ function ThemeSection({
     </Form>
   );
 }
+function ThemeToken({ token, type, tokens, setToken }) {
+  const value = tokens[token];
 
-function ThemeToken({
-  token,
-  type,
-  tokens,
-  setTokens,
-}: {
-  token: string;
-  type: 'color' | 'number' | 'string';
-  tokens: Record<string, any>;
-  setTokens: React.Dispatch<React.SetStateAction<Record<string, any>>>;
-}) {
-  const initialValue = tokens[token];
-  const [value, setValue] = useState(initialValue);
-
-  const handleChange = (val: any) => {
+  const handleChange = val => {
     const normalized =
       type === 'number'
         ? typeof val === 'number' && !Number.isNaN(val)
           ? val
           : null
         : val;
-    setValue(normalized);
-    setTokens(prev => ({ ...prev, [token]: normalized }));
+    setToken(token, normalized);
   };
 
   const renderInput = () => {
+    const commonProps = {
+      id: `token-input-${token}`,
+    };
+
     switch (type) {
       case 'color':
         return (
           <ColorPicker
+            {...commonProps}
             value={value}
             onChange={(_, hex) => handleChange(hex)}
             format="hex"
@@ -246,6 +260,7 @@ function ThemeToken({
       case 'number':
         return (
           <InputNumber
+            {...commonProps}
             style={{ width: '100%' }}
             value={value}
             onChange={handleChange}
@@ -254,7 +269,11 @@ function ThemeToken({
       case 'string':
       default:
         return (
-          <Input value={value} onChange={e => handleChange(e.target.value)} />
+          <Input
+            {...commonProps}
+            value={value}
+            onChange={e => handleChange(e.target.value)}
+          />
         );
     }
   };
@@ -267,10 +286,8 @@ function ThemeToken({
       : 160;
 
   return (
-    <div style={{ width, marginBottom: 0 }}>
-      <label style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>
-        {token}
-      </label>
+    <div style={{ width }}>
+      <div style={{ fontSize: 12, marginBottom: 4 }}>{token}</div>
       {renderInput()}
     </div>
   );