mirror of https://github.com/apache/superset.git
fix (dataset editor): allow Source tab readOnly mode (#11781)
* fix (dataset editor) add read-only mode for Source tab * add feature flag, add unit tests * rebase and fix comment * add message for padlock * move padlock to the bottom of tab
This commit is contained in:
parent
ac9761c730
commit
f292015ccd
|
@ -21,11 +21,15 @@ import { shallow } from 'enzyme';
|
|||
import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Radio } from 'react-bootstrap';
|
||||
|
||||
import Icon from 'src/components/Icon';
|
||||
import Tabs from 'src/common/components/Tabs';
|
||||
import DatasourceEditor from 'src/datasource/DatasourceEditor';
|
||||
import Field from 'src/CRUD/Field';
|
||||
import mockDatasource from 'spec/fixtures/mockDatasource';
|
||||
import * as featureFlags from 'src/featureFlags';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
|
||||
const props = {
|
||||
datasource: mockDatasource['7__table'],
|
||||
|
@ -44,6 +48,7 @@ describe('DatasourceEditor', () => {
|
|||
let wrapper;
|
||||
let el;
|
||||
let inst;
|
||||
let isFeatureEnabledMock;
|
||||
|
||||
beforeEach(() => {
|
||||
el = <DatasourceEditor {...props} />;
|
||||
|
@ -142,4 +147,65 @@ describe('DatasourceEditor', () => {
|
|||
wrapper.find(Field).find({ fieldKey: 'fetch_values_predicate' }).exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
describe('enable edit Source tab', () => {
|
||||
beforeAll(() => {
|
||||
isFeatureEnabledMock = jest
|
||||
.spyOn(featureFlags, 'isFeatureEnabled')
|
||||
.mockImplementation(
|
||||
feature => feature === 'ENABLE_DATASET_SOURCE_EDIT',
|
||||
);
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
isFeatureEnabledMock.mockRestore();
|
||||
});
|
||||
|
||||
it('Source Tab: edit mode', () => {
|
||||
wrapper.setState({ activeTabKey: 0, isEditMode: true });
|
||||
const sourceTab = wrapper.find(Tabs.TabPane).first();
|
||||
expect(sourceTab.find(Radio).first().prop('disabled')).toBe(false);
|
||||
|
||||
const icon = sourceTab.find(Icon);
|
||||
expect(icon.prop('name')).toBe('lock-unlocked');
|
||||
|
||||
const tableSelector = sourceTab.find(Field).shallow().find(TableSelector);
|
||||
expect(tableSelector.length).toBe(1);
|
||||
expect(tableSelector.prop('readOnly')).toBe(false);
|
||||
});
|
||||
|
||||
it('Source Tab: readOnly mode', () => {
|
||||
const sourceTab = wrapper.find(Tabs.TabPane).first();
|
||||
expect(sourceTab.find(Radio).length).toBe(2);
|
||||
expect(sourceTab.find(Radio).first().prop('disabled')).toBe(true);
|
||||
|
||||
const icon = sourceTab.find(Icon);
|
||||
expect(icon.prop('name')).toBe('lock-locked');
|
||||
icon.parent().simulate('click');
|
||||
expect(wrapper.state('isEditMode')).toBe(true);
|
||||
|
||||
const tableSelector = sourceTab.find(Field).shallow().find(TableSelector);
|
||||
expect(tableSelector.length).toBe(1);
|
||||
expect(tableSelector.prop('readOnly')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('disable edit Source tab', () => {
|
||||
// when edit is disabled, show readOnly controls and no padlock
|
||||
isFeatureEnabledMock = jest
|
||||
.spyOn(featureFlags, 'isFeatureEnabled')
|
||||
.mockImplementation(() => true);
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
wrapper.setState({ activeTabKey: 0 });
|
||||
|
||||
const sourceTab = wrapper.find(Tabs.TabPane).first();
|
||||
expect(sourceTab.find(Radio).length).toBe(2);
|
||||
expect(sourceTab.find(Radio).first().prop('disabled')).toBe(true);
|
||||
|
||||
const icon = sourceTab.find(Icon);
|
||||
expect(icon).toHaveLength(0);
|
||||
|
||||
isFeatureEnabledMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,6 +64,7 @@ interface DatabaseSelectorProps {
|
|||
onDbChange?: (db: any) => void;
|
||||
onSchemaChange?: (arg0?: any) => {};
|
||||
onSchemasLoad?: (schemas: Array<object>) => void;
|
||||
readOnly?: boolean;
|
||||
schema?: string;
|
||||
sqlLabMode?: boolean;
|
||||
onChange?: ({
|
||||
|
@ -87,6 +88,7 @@ export default function DatabaseSelector({
|
|||
onDbChange,
|
||||
onSchemaChange,
|
||||
onSchemasLoad,
|
||||
readOnly = false,
|
||||
schema,
|
||||
sqlLabMode = false,
|
||||
}: DatabaseSelectorProps) {
|
||||
|
@ -237,7 +239,7 @@ export default function DatabaseSelector({
|
|||
mutator={dbMutator}
|
||||
placeholder={t('Select a database')}
|
||||
autoSelect
|
||||
isDisabled={!isDatabaseSelectEnabled}
|
||||
isDisabled={!isDatabaseSelectEnabled || readOnly}
|
||||
/>,
|
||||
null,
|
||||
);
|
||||
|
@ -245,7 +247,7 @@ export default function DatabaseSelector({
|
|||
|
||||
function renderSchemaSelect() {
|
||||
const value = schemaOptions.filter(({ value }) => currentSchema === value);
|
||||
const refresh = !formMode && (
|
||||
const refresh = !formMode && !readOnly && (
|
||||
<RefreshLabel
|
||||
onClick={() => changeDataBase({ id: dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
|
@ -266,6 +268,7 @@ export default function DatabaseSelector({
|
|||
isLoading={schemaLoading}
|
||||
autosize={false}
|
||||
onChange={item => changeSchema(item)}
|
||||
isDisabled={readOnly}
|
||||
/>,
|
||||
refresh,
|
||||
);
|
||||
|
|
|
@ -97,6 +97,7 @@ interface TableSelectorProps {
|
|||
onSchemasLoad?: () => void;
|
||||
onTableChange?: (tableName: string, schema: string) => void;
|
||||
onTablesLoad?: (options: Array<any>) => {};
|
||||
readOnly?: boolean;
|
||||
schema?: string;
|
||||
sqlLabMode?: boolean;
|
||||
tableName?: string;
|
||||
|
@ -116,6 +117,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
onSchemasLoad,
|
||||
onTableChange,
|
||||
onTablesLoad,
|
||||
readOnly = false,
|
||||
schema,
|
||||
sqlLabMode = true,
|
||||
tableName,
|
||||
|
@ -286,28 +288,22 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
getTableList={fetchTables}
|
||||
handleError={handleError}
|
||||
onChange={onSelectionChange}
|
||||
onDbChange={onDbChange}
|
||||
onSchemaChange={onSchemaChange}
|
||||
onDbChange={readOnly ? undefined : onDbChange}
|
||||
onSchemaChange={readOnly ? undefined : onSchemaChange}
|
||||
onSchemasLoad={onSchemasLoad}
|
||||
schema={currentSchema}
|
||||
sqlLabMode={sqlLabMode}
|
||||
isDatabaseSelectEnabled={isDatabaseSelectEnabled}
|
||||
isDatabaseSelectEnabled={isDatabaseSelectEnabled && !readOnly}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTableSelect() {
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (database && database.allow_multi_schema_metadata_fetch) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
const options = tableOptions;
|
||||
let select = null;
|
||||
if (currentSchema && !formMode) {
|
||||
// dataset editor
|
||||
select = (
|
||||
<Select
|
||||
name="select-table"
|
||||
|
@ -321,6 +317,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
value={currentTableName}
|
||||
optionRenderer={renderTableOption}
|
||||
valueRenderer={renderTableOption}
|
||||
isDisabled={readOnly}
|
||||
/>
|
||||
);
|
||||
} else if (formMode) {
|
||||
|
@ -339,6 +336,15 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
// sql lab
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (database && database.allow_multi_schema_metadata_fetch) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
select = (
|
||||
<AsyncSelect
|
||||
name="async-select-table"
|
||||
|
@ -353,7 +359,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
/>
|
||||
);
|
||||
}
|
||||
const refresh = !formMode && (
|
||||
const refresh = !formMode && !readOnly && (
|
||||
<RefreshLabel
|
||||
onClick={() => changeSchema({ value: schema }, true)}
|
||||
tooltipContent={t('Force refresh table list')}
|
||||
|
|
|
@ -20,12 +20,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Alert, Badge, Col, Radio, Well } from 'react-bootstrap';
|
||||
import shortid from 'shortid';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import { styled, SupersetClient, t, supersetTheme } from '@superset-ui/core';
|
||||
|
||||
import Tabs from 'src/common/components/Tabs';
|
||||
import Button from 'src/components/Button';
|
||||
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||
import Icon from 'src/components/Icon';
|
||||
import Label from 'src/components/Label';
|
||||
import Loading from 'src/components/Loading';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
|
@ -45,6 +46,7 @@ import Fieldset from 'src/CRUD/Fieldset';
|
|||
import Field from 'src/CRUD/Field';
|
||||
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||
|
||||
const DatasourceContainer = styled.div`
|
||||
.change-warning {
|
||||
|
@ -66,6 +68,15 @@ const FlexRowContainer = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const EditLockContainer = styled.div`
|
||||
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
a {
|
||||
padding: 0 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const checkboxGenerator = (d, onChange) => (
|
||||
<CheckboxControl value={d} onChange={onChange} />
|
||||
);
|
||||
|
@ -279,6 +290,7 @@ class DatasourceEditor extends React.PureComponent {
|
|||
isSqla:
|
||||
props.datasource.datasource_type === 'table' ||
|
||||
props.datasource.type === 'table',
|
||||
isEditMode: false,
|
||||
databaseColumns: props.datasource.columns.filter(col => !col.expression),
|
||||
calculatedColumns: props.datasource.columns.filter(
|
||||
col => !!col.expression,
|
||||
|
@ -291,12 +303,16 @@ class DatasourceEditor extends React.PureComponent {
|
|||
};
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onChangeEditMode = this.onChangeEditMode.bind(this);
|
||||
this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
|
||||
this.onDatasourceChange = this.onDatasourceChange.bind(this);
|
||||
this.syncMetadata = this.syncMetadata.bind(this);
|
||||
this.setColumns = this.setColumns.bind(this);
|
||||
this.validateAndChange = this.validateAndChange.bind(this);
|
||||
this.handleTabSelect = this.handleTabSelect.bind(this);
|
||||
this.allowEditSource = !isFeatureEnabled(
|
||||
FeatureFlag.DISABLE_DATASET_SOURCE_EDIT,
|
||||
);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
|
@ -315,6 +331,10 @@ class DatasourceEditor extends React.PureComponent {
|
|||
this.props.onChange(newDatasource, this.state.errors);
|
||||
}
|
||||
|
||||
onChangeEditMode() {
|
||||
this.setState(prevState => ({ isEditMode: !prevState.isEditMode }));
|
||||
}
|
||||
|
||||
onDatasourceChange(datasource) {
|
||||
this.setState({ datasource }, this.validateAndChange);
|
||||
}
|
||||
|
@ -636,6 +656,7 @@ class DatasourceEditor extends React.PureComponent {
|
|||
inline
|
||||
onChange={this.onDatasourceTypeChange.bind(this, type.key)}
|
||||
checked={this.state.datasourceType === type.key}
|
||||
disabled={!this.state.isEditMode}
|
||||
>
|
||||
{type.label}
|
||||
</Radio>
|
||||
|
@ -655,13 +676,16 @@ class DatasourceEditor extends React.PureComponent {
|
|||
dbId={datasource.database.id}
|
||||
schema={datasource.schema}
|
||||
onSchemaChange={schema =>
|
||||
this.state.isEditMode &&
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
}
|
||||
onDbChange={database =>
|
||||
this.state.isEditMode &&
|
||||
this.onDatasourcePropChange('database', database)
|
||||
}
|
||||
formMode={false}
|
||||
handleError={this.props.addDangerToast}
|
||||
readOnly={!this.state.isEditMode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -675,6 +699,7 @@ class DatasourceEditor extends React.PureComponent {
|
|||
this.onDatasourcePropChange('table_name', table);
|
||||
}}
|
||||
placeholder={t('dataset name')}
|
||||
disabled={!this.state.isEditMode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -692,6 +717,7 @@ class DatasourceEditor extends React.PureComponent {
|
|||
offerEditInModal={false}
|
||||
minLines={25}
|
||||
maxLines={25}
|
||||
readOnly={!this.state.isEditMode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -727,16 +753,25 @@ class DatasourceEditor extends React.PureComponent {
|
|||
schema={datasource.schema}
|
||||
sqlLabMode={false}
|
||||
tableName={datasource.table_name}
|
||||
onSchemaChange={schema =>
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
onSchemaChange={
|
||||
this.state.isEditMode
|
||||
? schema =>
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
: undefined
|
||||
}
|
||||
onDbChange={database =>
|
||||
this.onDatasourcePropChange('database', database)
|
||||
onDbChange={
|
||||
this.state.isEditMode
|
||||
? database =>
|
||||
this.onDatasourcePropChange('database', database)
|
||||
: undefined
|
||||
}
|
||||
onTableChange={table => {
|
||||
this.onDatasourcePropChange('table_name', table);
|
||||
}}
|
||||
isDatabaseSelectEnabled={false}
|
||||
onTableChange={
|
||||
this.state.isEditMode
|
||||
? table =>
|
||||
this.onDatasourcePropChange('table_name', table)
|
||||
: undefined
|
||||
}
|
||||
readOnly={!this.state.isEditMode}
|
||||
/>
|
||||
}
|
||||
description={t(
|
||||
|
@ -749,6 +784,22 @@ class DatasourceEditor extends React.PureComponent {
|
|||
</Col>
|
||||
)}
|
||||
</Fieldset>
|
||||
{this.allowEditSource && (
|
||||
<EditLockContainer>
|
||||
<a href="#" onClick={this.onChangeEditMode}>
|
||||
<Icon
|
||||
color={supersetTheme.colors.grayscale.base}
|
||||
name={this.state.isEditMode ? 'lock-unlocked' : 'lock-locked'}
|
||||
/>
|
||||
</a>
|
||||
{!this.state.isEditMode && (
|
||||
<div>{t('Click the lock to make changes.')}</div>
|
||||
)}
|
||||
{this.state.isEditMode && (
|
||||
<div>{t('Click the lock to prevent further changes.')}</div>
|
||||
)}
|
||||
</EditLockContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ export enum FeatureFlag {
|
|||
THUMBNAILS = 'THUMBNAILS',
|
||||
LISTVIEWS_DEFAULT_CARD_VIEW = 'LISTVIEWS_DEFAULT_CARD_VIEW',
|
||||
ENABLE_REACT_CRUD_VIEWS = 'ENABLE_REACT_CRUD_VIEWS',
|
||||
DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT',
|
||||
DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML',
|
||||
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
|
||||
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
|
||||
|
|
|
@ -304,6 +304,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
|||
"ALLOW_DASHBOARD_DOMAIN_SHARDING": True,
|
||||
# Experimental feature introducing a client (browser) cache
|
||||
"CLIENT_CACHE": False,
|
||||
"DISABLE_DATASET_SOURCE_EDIT": False,
|
||||
"ENABLE_EXPLORE_JSON_CSRF_PROTECTION": False,
|
||||
"ENABLE_TEMPLATE_PROCESSING": False,
|
||||
"KV_STORE": False,
|
||||
|
|
|
@ -70,6 +70,7 @@ FRONTEND_CONF_KEYS = (
|
|||
"SUPERSET_DASHBOARD_POSITION_DATA_LIMIT",
|
||||
"SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT",
|
||||
"SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE",
|
||||
"DISABLE_DATASET_SOURCE_EDIT",
|
||||
"ENABLE_JAVASCRIPT_CONTROLS",
|
||||
"DEFAULT_SQLLAB_LIMIT",
|
||||
"SQL_MAX_ROW",
|
||||
|
|
Loading…
Reference in New Issue