codeant-ai-for-open-source[bot] commented on code in PR #36390:
URL: https://github.com/apache/superset/pull/36390#discussion_r2583772656
##########
superset-frontend/src/dashboard/util/constants.ts:
##########
@@ -31,6 +31,9 @@ export const NEW_MARKDOWN_ID = 'NEW_MARKDOWN_ID';
export const NEW_ROW_ID = 'NEW_ROW_ID';
export const NEW_TAB_ID = 'NEW_TAB_ID';
export const NEW_TABS_ID = 'NEW_TABS_ID';
+export const NEW_BUTTON_ID = 'NEW_BUTTON_ID';
+export const NEW_MODEL3D_ID = 'NEW_MODEL3D_ID';
+export const NEW_ALERTS_ID = 'NEW_ALERTS_ID';
Review Comment:
**Suggestion:** The newly added constants for button, 3D model, and alerts
IDs are not referenced anywhere else in the codebase, which introduces dead
code and can confuse future maintainers; they should be removed until they are
actually needed and wired into the dashboard logic. [code quality]
**Severity Level:** Minor ⚠️
```suggestion
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
A search across the repository confirms these three newly added ID
constants are not referenced anywhere else, so they currently serve no
purpose and just clutter the constants module. Removing them until the
associated features are implemented avoids dead exports that suggest
non‑existent functionality. This is a reasonable, concrete clean‑up and
does not conflict with the PR's changes.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/dashboard/util/constants.ts
**Line:** 34:36
**Comment:**
*Code Quality: The newly added constants for button, 3D model, and
alerts IDs are not referenced anywhere else in the codebase, which introduces
dead code and can confuse future maintainers; they should be removed until they
are actually needed and wired into the dashboard logic.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/dashboard/components/gridComponents/Alerts/AlertsConfigMenuItem.tsx:
##########
@@ -0,0 +1,286 @@
+/**
+ * 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 { useCallback, useMemo, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { styled, css } from '@apache-superset/core/ui';
+import { Button as SupersetButton } from '@superset-ui/core/components/Button';
+import { Form } from '@superset-ui/core/components/Form';
+import { Input, Select, Tooltip } from '@superset-ui/core/components';
+import { Switch } from '@superset-ui/core/components/Switch';
+import { Modal } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import type { DashboardAlertsMeta } from './types';
+
+const MenuTrigger = styled.div`
+ ${({ theme }) => css`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: ${theme.colorTextDescription};
+
+ &:hover {
+ color: ${theme.colorPrimary};
+ }
+ `}
+`;
+
+const ConfigFormWrapper = styled.div`
+ ${({ theme }) => css`
+ .ant-form-item {
+ margin-bottom: ${theme.sizeUnit * 3}px;
+ }
+
+ .config-form-footer {
+ display: flex;
+ gap: ${theme.sizeUnit * 2}px;
+ justify-content: flex-end;
+ margin-top: ${theme.sizeUnit * 4}px;
+ padding-top: ${theme.sizeUnit * 3}px;
+ border-top: 1px solid ${theme.colorBorder};
+ }
+
+ .form-section-title {
+ font-weight: ${theme.fontWeightBold};
+ margin-bottom: ${theme.sizeUnit * 2}px;
+ color: ${theme.colorText};
+ }
+
+ .form-help-text {
+ font-size: ${theme.fontSizeSm}px;
+ color: ${theme.colorTextDescription};
+ margin-top: ${theme.sizeUnit}px;
+ }
+ `}
+`;
+
+export type AlertsConfig = Pick<
+ DashboardAlertsMeta,
+ | 'mqttTopic'
+ | 'includeGlobalTopic'
+ | 'eventFilter'
+ | 'severityFilter'
+ | 'showVisualIndicator'
+ | 'indicatorColor'
+>;
+
+type FormValues = AlertsConfig;
+
+interface AlertsConfigMenuItemProps {
+ meta: DashboardAlertsMeta;
+ onSave: (updates: AlertsConfig) => void;
+ onVisibilityChange?: (open: boolean) => void;
+}
+
+function getInitialValues(meta: DashboardAlertsMeta): FormValues {
+ return {
+ mqttTopic: meta.mqttTopic ?? '',
+ includeGlobalTopic: meta.includeGlobalTopic ?? true,
+ eventFilter: meta.eventFilter ?? '',
+ severityFilter: meta.severityFilter ?? [],
+ showVisualIndicator: meta.showVisualIndicator ?? true,
+ indicatorColor: meta.indicatorColor ?? '#1890ff',
+ };
+}
+
+const AlertsConfigMenuItem = ({
+ meta,
+ onSave,
+ onVisibilityChange,
+}: AlertsConfigMenuItemProps) => {
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form] = Form.useForm<FormValues>();
+
+ const initialValues = useMemo(() => getInitialValues(meta), [meta]);
+
+ const severityOptions = useMemo(
+ () => [
+ { value: 'info', label: t('Info') },
+ { value: 'success', label: t('Success') },
+ { value: 'warning', label: t('Warning') },
+ { value: 'error', label: t('Error') },
+ ],
+ [],
+ );
+
+ const handleOpenModal = useCallback(() => {
+ form.setFieldsValue(initialValues);
+ setModalVisible(true);
+ onVisibilityChange?.(true);
+ }, [form, initialValues, onVisibilityChange]);
+
+ const handleCloseModal = useCallback(() => {
+ setModalVisible(false);
+ onVisibilityChange?.(false);
+ }, [onVisibilityChange]);
+
+ const handleSubmit = useCallback(() => {
+ form
+ .validateFields()
+ .then(values => {
+ const updates: AlertsConfig = {
+ mqttTopic: values.mqttTopic?.trim() || '',
+ includeGlobalTopic: values.includeGlobalTopic ?? true,
+ eventFilter: values.eventFilter?.trim() || '',
+ severityFilter: values.severityFilter || [],
+ showVisualIndicator: values.showVisualIndicator ?? true,
+ indicatorColor: values.indicatorColor || '#1890ff',
+ };
+ onSave(updates);
+ handleCloseModal();
+ })
+ .catch(error => {
+ console.error('Form validation failed:', error);
+ });
+ }, [form, onSave, handleCloseModal]);
+
+ return (
+ <>
+ <Tooltip title={t('Configure alerts')} placement="top">
+ <MenuTrigger onClick={handleOpenModal}>
+ <Icons.SettingOutlined />
+ </MenuTrigger>
+ </Tooltip>
+
+ <Modal
+ title={t('Configure Alert Listener')}
+ visible={modalVisible}
+ onCancel={handleCloseModal}
+ width={600}
+ footer={null}
+ destroyOnClose
+ >
+ <ConfigFormWrapper>
+ <Form
+ form={form}
+ layout="vertical"
+ initialValues={initialValues}
+ preserve={false}
+ >
+ {/* MQTT Topic Configuration */}
+ <div className="form-section-title">{t('MQTT Topic')}</div>
+
+ <Form.Item
+ name="mqttTopic"
+ label={t('Custom Topic')}
+ extra={t('Enter the MQTT topic to subscribe to (e.g.,
"temperature/alerts", "dashboard/123/events")')}
+ rules={[
+ {
+ validator: (_, value) => {
+ if (!value || value.trim().length === 0) {
+ return Promise.reject(
+ new Error(t('Please enter an MQTT topic')),
+ );
+ }
+ // Basic topic validation
+ if (value.includes('#') && !value.endsWith('#')) {
Review Comment:
**Suggestion:** The MQTT topic validator operates on the raw input string
instead of a trimmed version, so a topic that is valid once trimmed (for
example, with trailing or leading spaces) will be rejected by validation even
though `handleSubmit` later trims and saves it, causing the UI to block valid
inputs and behave inconsistently with the saved value. [logic error]
**Severity Level:** Minor ⚠️
```suggestion
const trimmedValue = (value || '').trim();
if (trimmedValue.length === 0) {
return Promise.reject(
new Error(t('Please enter an MQTT topic')),
);
}
// Basic topic validation
if (
trimmedValue.includes('#') &&
!trimmedValue.endsWith('#')
) {
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The current validator mixes trimmed and untrimmed values: emptiness is
checked on `value.trim()`, but the wildcard rule uses the raw `value`. That
means an input like `"topic/# "` will be rejected by the validator (because it
doesn't literally end with `'#'`), even though `handleSubmit` later trims and
persists `"topic/#"`, which is valid. That's a real logic/UX bug where
validation blocks a value that the submit logic would normalize and accept. The
suggested change consistently trims once up front and applies both checks to
the trimmed string, aligning validation with the saved value and avoiding
spurious rejections, without introducing any nonsense.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/dashboard/components/gridComponents/Alerts/AlertsConfigMenuItem.tsx
**Line:** 186:192
**Comment:**
*Logic Error: The MQTT topic validator operates on the raw input string
instead of a trimmed version, so a topic that is valid once trimmed (for
example, with trailing or leading spaces) will be rejected by validation even
though `handleSubmit` later trims and saves it, causing the UI to block valid
inputs and behave inconsistently with the saved value.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/dashboard/components/gridComponents/Button/ButtonConfigMenuItem.tsx:
##########
@@ -0,0 +1,491 @@
+/**
+ * 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 { useCallback, useEffect, useMemo, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { styled, css } from '@apache-superset/core/ui';
+import { Button as SupersetButton } from '@superset-ui/core/components/Button';
+import { Form } from '@superset-ui/core/components/Form';
+import { Input, Select, Tooltip } from '@superset-ui/core/components';
+import { Switch } from '@superset-ui/core/components/Switch';
+import { Modal } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import type {
+ ButtonSize,
+ ButtonStyle,
+} from '@superset-ui/core/components/Button/types';
+import type { DashboardButtonMeta } from './types';
+
+const MenuTrigger = styled.div`
+ ${({ theme }) => css`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: ${theme.colorTextDescription};
+
+ &:hover {
+ color: ${theme.colorPrimary};
+ }
+ `}
+`;
+
+const ConfigFormWrapper = styled.div`
+ ${({ theme }) => css`
+ width: 320px;
+
+ .ant-form-item {
+ margin-bottom: ${theme.sizeUnit * 2}px;
+ }
+
+ .config-form-footer {
+ display: flex;
+ gap: ${theme.sizeUnit * 2}px;
+ justify-content: flex-end;
+ margin-top: ${theme.sizeUnit * 3}px;
+ }
+ `}
+`;
+
+const TextArea = styled(Input.TextArea)`
+ ${({ theme }) => css`
+ resize: vertical;
+ min-height: ${theme.sizeUnit * 10}px;
+ `}
+`;
+
+// Removed unused constants - using inline options in Select components
+
+export type ButtonConfig = Pick<
+ DashboardButtonMeta,
+ | 'buttonStyle'
+ | 'buttonSize'
+ | 'disabled'
+ | 'tooltip'
+ | 'actionType'
+ | 'url'
+ | 'target'
+ | 'apiEndpoint'
+ | 'apiMethod'
+ | 'apiHeaders'
+ | 'apiPayload'
+ | 'successMessage'
+ | 'errorMessage'
+ | 'confirmBeforeExecute'
+ | 'confirmMessage'
+>;
+
+type FormValues = ButtonConfig;
+
+interface ButtonConfigMenuItemProps {
+ meta: DashboardButtonMeta;
+ onSave: (updates: ButtonConfig) => void;
+ onVisibilityChange?: (open: boolean) => void;
+}
+
+const normalizeButtonSize = (size?: string | null): ButtonSize =>
+ size === 'middle' || !size ? 'default' : (size as ButtonSize);
+
+const normalizeButtonStyle = (style?: string | null): ButtonStyle =>
+ (style as ButtonStyle) || 'primary';
+
+const normalizeActionType = (
+ actionType?: string | null,
+): ButtonConfig['actionType'] =>
+ actionType === 'api' ? 'api' : 'link';
+
+function getInitialValues(meta: DashboardButtonMeta) {
+ return {
+ buttonStyle: normalizeButtonStyle(meta.buttonStyle),
+ buttonSize: normalizeButtonSize(meta.buttonSize),
+ disabled: meta.disabled ?? false,
+ tooltip: meta.tooltip ?? '',
+ actionType: normalizeActionType(meta.actionType),
+ url: meta.url ?? '',
+ target: meta.target ?? '_self',
+ apiEndpoint: meta.apiEndpoint ?? '',
+ apiMethod: (meta.apiMethod ?? 'POST').toUpperCase(),
+ apiHeaders: meta.apiHeaders ?? '',
+ apiPayload: meta.apiPayload ?? '',
+ successMessage:
+ meta.successMessage ?? t('Action executed successfully.'),
+ errorMessage:
+ meta.errorMessage ?? t('Unable to execute action.'),
+ confirmBeforeExecute: meta.confirmBeforeExecute ?? false,
+ confirmMessage:
+ meta.confirmMessage ?? t('Are you sure you want to run this action?'),
+ } satisfies FormValues;
+}
+
+const ButtonConfigMenuItem = ({
+ meta,
+ onSave,
+ onVisibilityChange,
+}: ButtonConfigMenuItemProps) => {
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form] = Form.useForm<FormValues>();
+
+ const initialValues = useMemo(() => getInitialValues(meta), [meta]);
+ const actionOptions = useMemo(
+ () => [
+ { value: 'link', label: t('Open link') },
+ { value: 'api', label: t('Call API') },
+ ],
+ [],
+ );
+ const buttonStyleOptions = useMemo(
+ () => [
+ { value: 'primary', label: t('Primary') },
+ { value: 'secondary', label: t('Secondary') },
+ { value: 'tertiary', label: t('Tertiary') },
+ { value: 'danger', label: t('Danger') },
+ { value: 'link', label: t('Link') },
+ { value: 'dashed', label: t('Dashed') },
+ ],
+ [],
+ );
+ const buttonSizeOptions = useMemo(
+ () => [
+ { value: 'default', label: t('Default') },
+ { value: 'small', label: t('Small') },
+ { value: 'xsmall', label: t('Extra small') },
+ ],
+ [],
+ );
+
+ const [actionType, setActionType] = useState(initialValues.actionType);
+
+ useEffect(() => {
+ if (modalVisible) {
+ form.setFieldsValue(initialValues);
+ setActionType(initialValues.actionType);
+ }
+ }, [form, initialValues, modalVisible]);
+
+ const closeModal = useCallback(
+ (shouldReset = false) => {
+ setModalVisible(false);
+ onVisibilityChange?.(false);
+ if (shouldReset) {
+ form.resetFields();
+ setActionType(initialValues.actionType);
+ }
+ },
+ [form, initialValues.actionType, onVisibilityChange],
+ );
+
+ const handleSubmit = useCallback(
+ async (values: FormValues) => {
+ onSave({
+ ...values,
+ apiMethod: values.apiMethod?.toUpperCase(),
+ url: values.url?.trim() ?? '',
+ apiEndpoint: values.apiEndpoint?.trim() ?? '',
+ apiHeaders: values.apiHeaders?.trim() ?? '',
+ apiPayload: values.apiPayload?.trim() ?? '',
+ tooltip: values.tooltip?.trim() ?? '',
+ successMessage: values.successMessage?.trim() ?? '',
+ errorMessage: values.errorMessage?.trim() ?? '',
+ confirmMessage: values.confirmMessage?.trim() ?? '',
+ });
+ closeModal(false);
+ },
+ [closeModal, onSave],
+ );
+
+ const handleCancel = useCallback(() => {
+ closeModal(true);
+ }, [closeModal]);
+
+ const handleOpenModal = useCallback(() => {
+ setModalVisible(true);
+ onVisibilityChange?.(true);
+ }, [onVisibilityChange]);
+
+ return (
+ <>
+ <Tooltip title={t('Configure button')}>
+ <MenuTrigger
+ onClick={event => {
+ event.stopPropagation();
+ handleOpenModal();
+ }}
+ >
+ <Icons.SettingOutlined iconSize="m" />
+ </MenuTrigger>
+ </Tooltip>
+ <Modal
+ title={t('Button configuration')}
+ show={modalVisible}
+ onHide={handleCancel}
+ footer={
+ <div className="config-form-footer">
+ <SupersetButton
+ buttonStyle="tertiary"
+ buttonSize="small"
+ onClick={handleCancel}
+ >
+ {t('Cancel')}
+ </SupersetButton>
+ <SupersetButton
+ buttonStyle="primary"
+ buttonSize="small"
+ onClick={() => form.submit()}
+ >
+ {t('Save')}
+ </SupersetButton>
+ </div>
+ }
+ >
+ <ConfigFormWrapper data-test="dashboard-button-config">
+ <Form
+ layout="vertical"
+ form={form}
+ initialValues={initialValues}
+ onFinish={handleSubmit}
+ onValuesChange={(changedValues, values) => {
+ if (
+ Object.prototype.hasOwnProperty.call(
+ changedValues,
+ 'actionType',
+ )
+ ) {
+ setActionType(
+ (values.actionType as ButtonConfig['actionType']) ?? 'link',
+ );
+ }
+ }}
+ >
+ <Form.Item<FormValues>
+ name="buttonStyle"
+ label={t('Button style')}
+ >
+ <Select
+ options={buttonStyleOptions}
+ />
+ </Form.Item>
+ <Form.Item<FormValues> name="buttonSize" label={t('Button size')}>
+ <Select
+ options={buttonSizeOptions}
+ />
+ </Form.Item>
+ <Form.Item<FormValues>
+ name="tooltip"
+ label={t('Tooltip')}
+ extra={t('Optional helper text displayed on hover')}
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item<FormValues>
+ name="disabled"
+ label={t('Disable button')}
+ valuePropName="checked"
+ >
+ <Switch />
+ </Form.Item>
+ <Form.Item<FormValues>
+ name="actionType"
+ label={t('Action type')}
+ >
+ <Select options={actionOptions} />
+ </Form.Item>
+ {actionType === 'link' && (
+ <>
+ <Form.Item<FormValues>
+ name="url"
+ label={t('Destination URL')}
+ rules={[
+ {
+ validator: (_, value) => {
+ if (!value || value.trim().length === 0) {
+ return Promise.resolve();
+ }
+ const trimmedValue = value.trim();
+ const trimmedLower = trimmedValue.toLowerCase();
+ // Block dangerous protocols
+ const dangerousProtocols = ['javascript:', 'data:',
'vbscript:', 'file:'];
+ if (dangerousProtocols.some(protocol =>
trimmedLower.startsWith(protocol))) {
+ return Promise.reject(
Review Comment:
**Suggestion:** The list of blocked URL protocols is duplicated in both the
link URL and API endpoint validators, which increases the risk that a future
security update will only change one copy and leave the other vulnerable;
extracting this list into a shared constant used by both validators avoids this
drift. [security]
**Severity Level:** Critical 🚨
```suggestion
const DANGEROUS_PROTOCOLS = ['javascript:', 'data:',
'vbscript:', 'file:'] as const;
// Block dangerous protocols
if (DANGEROUS_PROTOCOLS.some(protocol =>
trimmedLower.startsWith(protocol))) {
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The same `['javascript:', 'data:', 'vbscript:', 'file:']` array is present
in both the link URL and API endpoint validators with identical semantics, so
this is real duplication in security-sensitive logic. Hoisting it into a shared
constant preserves behavior today while reducing the chance that a future
protocol change gets applied to only one of the two validators. That's a
legitimate code-quality improvement by removing duplicated logic, not just
cosmetic churn.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/dashboard/components/gridComponents/Button/ButtonConfigMenuItem.tsx
**Line:** 321:323
**Comment:**
*Security: The list of blocked URL protocols is duplicated in both the
link URL and API endpoint validators, which increases the risk that a future
security update will only change one copy and leave the other vulnerable;
extracting this list into a shared constant used by both validators avoids this
drift.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]