/* eslint-disable camelcase */ /** * 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, { useState, useCallback } from 'react'; import { DatasourceType, SupersetClient, Datasource } from '@superset-ui/core'; import { t } from '@apache-superset/core/translation'; import { css, styled, useTheme } from '@apache-superset/core/theme'; import { getTemporalColumns } from '@superset-ui/chart-controls'; import { getUrlParam } from 'src/utils/urlUtils'; import { Dropdown, Tooltip, Button, ModalTrigger, } from '@superset-ui/core/components'; import { ChangeDatasourceModal, DatasourceModal, ErrorAlert, } from 'src/components'; import { Menu } from '@superset-ui/core/components/Menu'; import { Icons } from '@superset-ui/core/components/Icons'; import WarningIconWithTooltip from '@superset-ui/core/components/WarningIconWithTooltip'; import { URL_PARAMS } from 'src/constants'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import { userHasPermission, isUserAdmin, } from 'src/dashboard/util/permissionUtils'; import { ErrorMessageWithStackTrace } from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter'; import ViewQuery from 'src/explore/components/controls/ViewQuery'; import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { safeStringify } from 'src/utils/safeStringify'; import { Link } from 'react-router-dom'; // Extended Datasource interface with all properties used in this component interface ExtendedDatasource extends Datasource { sql?: string; select_star?: string; owners?: Array<{ id: number; first_name: string; last_name: string; value?: number; }>; extra?: string; health_check_message?: string; database?: { id: number; database_name: string; backend?: string; }; } interface User { userId?: number; username?: string; roles?: Record; } interface DatasourceControlActions { changeDatasource: (datasource: ExtendedDatasource) => void; setControlValue: (name: string, value: unknown) => void; } interface FormData { granularity_sqla?: string; [key: string]: unknown; } interface DatasourceControlProps { actions: DatasourceControlActions; onChange?: () => void; value?: string | null; datasource: ExtendedDatasource; form_data?: FormData; isEditable?: boolean; onDatasourceSave?: ((datasource: ExtendedDatasource) => void) | null; user: User; // ControlHeader-related props hovered?: boolean; type?: string; label?: string; default?: unknown; description?: string | null; validationErrors?: string[]; name?: string; } const getDatasetType = (datasource: ExtendedDatasource): string => { if (datasource.type === 'query') { return 'query'; } if (datasource.type === 'table' && datasource.sql) { return 'virtual_dataset'; } return 'physical_dataset'; }; const Styles = styled.div` .data-container { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid ${({ theme }) => theme.colorSplit}; padding: ${({ theme }) => 4 * theme.sizeUnit}px; padding-right: ${({ theme }) => 2 * theme.sizeUnit}px; } .error-alert { margin: ${({ theme }) => 2 * theme.sizeUnit}px; min-height: 150px; } .ant-dropdown-trigger { margin-left: ${({ theme }) => 2 * theme.sizeUnit}px; } .btn-group .open .dropdown-toggle { box-shadow: none; &.button-default { background: none; } } i.angle { color: ${({ theme }) => theme.colorPrimary}; } svg.datasource-modal-trigger { color: ${({ theme }) => theme.colorPrimary}; cursor: pointer; } .title-select { flex: 1 1 100%; display: inline-block; padding: ${({ theme }) => theme.sizeUnit * 2}px 0px; border-radius: ${({ theme }) => theme.borderRadius}px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .datasource-svg { margin-right: ${({ theme }) => 2 * theme.sizeUnit}px; flex: none; } span[aria-label='dataset-physical'] { color: ${({ theme }) => theme.colorIcon}; } span[aria-label='more'] { color: ${({ theme }) => theme.colorPrimary}; } `; const CHANGE_DATASET = 'change_dataset'; const VIEW_IN_SQL_LAB = 'view_in_sql_lab'; const EDIT_DATASET = 'edit_dataset'; const QUERY_PREVIEW = 'query_preview'; const SAVE_AS_DATASET = 'save_as_dataset'; // If the string is longer than this value's number characters we add // a tooltip for user can see the full name by hovering over the visually truncated string in UI const VISIBLE_TITLE_LENGTH = 25; // Assign icon for each DatasourceType. If no icon assignment is found in the lookup, no icon will render export const datasourceIconLookup: Record = { query: , physical_dataset: , virtual_dataset: , }; // Render title for datasource with tooltip only if text is longer than VISIBLE_TITLE_LENGTH export const renderDatasourceTitle = ( displayString: string | undefined, tooltip: string, ) => displayString?.length && displayString.length > VISIBLE_TITLE_LENGTH ? ( // Add a tooltip only for long names that will be visually truncated {displayString} ) : ( {displayString} ); // Different data source types use different attributes for the display title export const getDatasourceTitle = ( datasource: ExtendedDatasource | null | undefined, ): string => { if (datasource?.type === 'query') return datasource?.sql || ''; return datasource?.name || ''; }; const preventRouterLinkWhileMetaClicked = (evt: React.MouseEvent) => { if (evt.metaKey) { evt.preventDefault(); } else { evt.stopPropagation(); } }; export default function DatasourceControl({ actions, onChange = () => {}, value = null, datasource, form_data, isEditable = true, onDatasourceSave = null, user, }: DatasourceControlProps) { const theme = useTheme(); const [showEditDatasourceModal, setShowEditDatasourceModal] = useState(false); const [showChangeDatasourceModal, setShowChangeDatasourceModal] = useState(false); const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); const handleDatasourceSave = useCallback( (savedDatasource: Datasource) => { // Cast to ExtendedDatasource for the component's internal use actions.changeDatasource(savedDatasource as ExtendedDatasource); // Cast datasource for getTemporalColumns which expects Dataset | QueryResponse const { temporalColumns, defaultTemporalColumn } = getTemporalColumns( savedDatasource as Parameters[0], ); const { columns } = savedDatasource; // the granularity_sqla might not be a temporal column anymore const timeCol = form_data?.granularity_sqla; const isGranularitySqlaTemporal = columns.find( ({ column_name }) => column_name === timeCol, )?.is_dttm; // the main_dttm_col might not be a temporal column anymore const isDefaultTemporal = columns.find( ({ column_name }) => column_name === defaultTemporalColumn, )?.is_dttm; // if granularity_sqla is empty or it is not a temporal column anymore // let's update the control value if (savedDatasource.type === 'table' && !isGranularitySqlaTemporal) { const temporalColumn = isDefaultTemporal ? defaultTemporalColumn : temporalColumns?.[0]; actions.setControlValue('granularity_sqla', temporalColumn || null); } if (onDatasourceSave) { onDatasourceSave(savedDatasource); } }, [actions, form_data?.granularity_sqla, onDatasourceSave], ); const toggleChangeDatasourceModal = useCallback(() => { setShowChangeDatasourceModal(prev => !prev); }, []); const toggleEditDatasourceModal = useCallback(() => { setShowEditDatasourceModal(prev => !prev); }, []); const toggleSaveDatasetModal = useCallback(() => { setShowSaveDatasetModal(prev => !prev); }, []); const handleMenuItemClick = useCallback( ({ key }: { key: string }) => { switch (key) { case CHANGE_DATASET: toggleChangeDatasourceModal(); break; case EDIT_DATASET: toggleEditDatasourceModal(); break; case VIEW_IN_SQL_LAB: { const payload = { datasourceKey: `${datasource.id}__${datasource.type}`, sql: datasource.sql, }; SupersetClient.postForm('/sqllab/', { form_data: safeStringify(payload), }); } break; case SAVE_AS_DATASET: toggleSaveDatasetModal(); break; default: break; } }, [ datasource, toggleChangeDatasourceModal, toggleEditDatasourceModal, toggleSaveDatasetModal, ], ); let extra; if (datasource?.extra) { if (typeof datasource.extra === 'string') { try { extra = JSON.parse(datasource.extra); } catch {} // eslint-disable-line no-empty } else { extra = datasource.extra; // eslint-disable-line prefer-destructuring } } const isMissingDatasource = !datasource?.id || Boolean(extra?.error); let isMissingParams = false; if (isMissingDatasource) { const datasourceId = getUrlParam(URL_PARAMS.datasourceId); const sliceId = getUrlParam(URL_PARAMS.sliceId); if (!datasourceId && !sliceId) { isMissingParams = true; } } const allowEdit = datasource.owners?.map(o => o.id || o.value).includes(user.userId) || isUserAdmin(user); const canAccessSqlLab = userHasPermission(user, 'SQL Lab', 'menu_access'); const editText = t('Edit dataset'); const requestedQuery = { datasourceKey: `${datasource.id}__${datasource.type}`, sql: datasource.sql, }; const defaultDatasourceMenuItems = []; if (isEditable && !isMissingDatasource) { defaultDatasourceMenuItems.push({ key: EDIT_DATASET, label: !allowEdit ? ( {editText} ) : ( editText ), disabled: !allowEdit, 'data-test': 'edit-dataset', }); } defaultDatasourceMenuItems.push({ key: CHANGE_DATASET, label: t('Swap dataset'), }); if (!isMissingDatasource && canAccessSqlLab) { defaultDatasourceMenuItems.push({ key: VIEW_IN_SQL_LAB, label: ( {t('View in SQL Lab')} ), }); } const defaultDatasourceMenu = ( ); const queryDatasourceMenuItems = [ { key: QUERY_PREVIEW, label: ( {t('Query preview')} } modalTitle={t('Query preview')} modalBody={ } modalFooter={ } draggable={false} resizable={false} responsive /> ), }, ]; if (canAccessSqlLab) { queryDatasourceMenuItems.push({ key: VIEW_IN_SQL_LAB, label: ( {t('View in SQL Lab')} ), }); } queryDatasourceMenuItems.push({ key: SAVE_AS_DATASET, label: {t('Save as dataset')}, }); const queryDatasourceMenu = ( ); const { health_check_message: healthCheckMessage } = datasource; const titleText = isMissingDatasource && !datasource.name ? t('Missing dataset') : getDatasourceTitle(datasource); const tooltip = titleText; return (
{datasourceIconLookup[getDatasetType(datasource)]} {renderDatasourceTitle(titleText, tooltip)} {healthCheckMessage && ( )} {extra?.warning_markdown && ( )} datasource.type === DatasourceType.Query ? queryDatasourceMenu : defaultDatasourceMenu } trigger={['click']} data-test="datasource-menu" >
{/* missing dataset */} {isMissingDatasource && isMissingParams && (
)} {isMissingDatasource && !isMissingParams && (
{extra?.error ? ( ) : (

{t( 'The dataset linked to this chart may have been deleted.', )}

} /> )}
)} {showEditDatasourceModal && ( )} {showChangeDatasourceModal && ( )} {showSaveDatasetModal && ( )}
); }