mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
feat: Dataset Creation Footer Component (#21241)
Co-authored-by: lyndsiWilliams <kcatgirl@gmail.com>
This commit is contained in:
parent
4913da1511
commit
c4638fa2b9
@ -35,6 +35,15 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart';
|
||||
export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART =
|
||||
'export_csv_dashboard_chart';
|
||||
export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter';
|
||||
export const LOG_ACTIONS_DATASET_CREATION_EMPTY_CANCELLATION =
|
||||
'dataset_creation_empty_cancellation';
|
||||
export const LOG_ACTIONS_DATASET_CREATION_DATABASE_CANCELLATION =
|
||||
'dataset_creation_database_cancellation';
|
||||
export const LOG_ACTIONS_DATASET_CREATION_SCHEMA_CANCELLATION =
|
||||
'dataset_creation_schema_cancellation';
|
||||
export const LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION =
|
||||
'dataset_creation_table_cancellation';
|
||||
export const LOG_ACTIONS_DATASET_CREATION_SUCCESS = 'dataset_creation_success';
|
||||
|
||||
// Log event types --------------------------------------------------------------
|
||||
export const LOG_EVENT_TYPE_TIMING = new Set([
|
||||
@ -56,6 +65,14 @@ export const LOG_EVENT_TYPE_USER = new Set([
|
||||
LOG_ACTIONS_MOUNT_EXPLORER,
|
||||
]);
|
||||
|
||||
export const LOG_EVENT_DATASET_TYPE_DATASET_CREATION = [
|
||||
LOG_ACTIONS_DATASET_CREATION_EMPTY_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_DATABASE_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_SCHEMA_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_SUCCESS,
|
||||
];
|
||||
|
||||
export const Logger = {
|
||||
timeOriginOffset: 0,
|
||||
|
||||
|
@ -35,7 +35,7 @@ describe('AddDataset', () => {
|
||||
// Left panel
|
||||
expect(blankeStateImgs[0]).toBeVisible();
|
||||
// Footer
|
||||
expect(screen.getByText(/footer/i)).toBeVisible();
|
||||
expect(screen.getByText(/Cancel/i)).toBeVisible();
|
||||
|
||||
expect(blankeStateImgs.length).toBe(1);
|
||||
});
|
||||
|
@ -20,10 +20,47 @@ import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import Footer from 'src/views/CRUD/data/dataset/AddDataset/Footer';
|
||||
|
||||
describe('Footer', () => {
|
||||
it('renders a blank state Footer', () => {
|
||||
render(<Footer />);
|
||||
const mockedProps = {
|
||||
url: 'realwebsite.com',
|
||||
};
|
||||
|
||||
expect(screen.getByText(/footer/i)).toBeVisible();
|
||||
const mockPropsWithDataset = {
|
||||
url: 'realwebsite.com',
|
||||
datasetObject: {
|
||||
database: {
|
||||
id: '1',
|
||||
database_name: 'examples',
|
||||
},
|
||||
owners: [1, 2, 3],
|
||||
schema: 'public',
|
||||
dataset_name: 'Untitled',
|
||||
table_name: 'real_info',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Footer', () => {
|
||||
it('renders a Footer with a cancel button and a disabled create button', () => {
|
||||
render(<Footer {...mockedProps} />, { useRedux: true });
|
||||
|
||||
const saveButton = screen.getByRole('button', {
|
||||
name: /Cancel/i,
|
||||
});
|
||||
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create/i,
|
||||
});
|
||||
|
||||
expect(saveButton).toBeVisible();
|
||||
expect(createButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders a Create Dataset button when a table is selected', () => {
|
||||
render(<Footer {...mockPropsWithDataset} />, { useRedux: true });
|
||||
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create/i,
|
||||
});
|
||||
|
||||
expect(createButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,104 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import {
|
||||
LOG_ACTIONS_DATASET_CREATION_EMPTY_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_DATABASE_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_SCHEMA_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_SUCCESS,
|
||||
} from 'src/logger/LogUtils';
|
||||
import { DatasetObject } from '../types';
|
||||
|
||||
export default function Footer() {
|
||||
return <div>Footer</div>;
|
||||
interface FooterProps {
|
||||
url: string;
|
||||
addDangerToast: () => void;
|
||||
datasetObject?: Partial<DatasetObject> | null;
|
||||
onDatasetAdd?: (dataset: DatasetObject) => void;
|
||||
}
|
||||
|
||||
const INPUT_FIELDS = ['db', 'schema', 'table_name'];
|
||||
const LOG_ACTIONS = [
|
||||
LOG_ACTIONS_DATASET_CREATION_EMPTY_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_DATABASE_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_SCHEMA_CANCELLATION,
|
||||
LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION,
|
||||
];
|
||||
|
||||
function Footer({ url, datasetObject, addDangerToast }: FooterProps) {
|
||||
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
|
||||
'dataset',
|
||||
t('dataset'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
const createLogAction = (dataset: Partial<DatasetObject>) => {
|
||||
let totalCount = 0;
|
||||
const value = Object.keys(dataset).reduce((total, key) => {
|
||||
if (INPUT_FIELDS.includes(key) && dataset[key]) {
|
||||
totalCount += 1;
|
||||
}
|
||||
return totalCount;
|
||||
}, 0);
|
||||
|
||||
return LOG_ACTIONS[value];
|
||||
};
|
||||
const goToPreviousUrl = () => {
|
||||
// this is a placeholder url until the final feature gets implemented
|
||||
// at that point we will be passing in the url of the previous location.
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
const cancelButtonOnClick = () => {
|
||||
if (!datasetObject) {
|
||||
logEvent(LOG_ACTIONS_DATASET_CREATION_EMPTY_CANCELLATION, {});
|
||||
} else {
|
||||
const logAction = createLogAction(datasetObject);
|
||||
logEvent(logAction, datasetObject);
|
||||
}
|
||||
goToPreviousUrl();
|
||||
};
|
||||
|
||||
const tooltipText = t('Select a database table.');
|
||||
|
||||
const onSave = () => {
|
||||
if (datasetObject) {
|
||||
const data = {
|
||||
database: datasetObject.db?.id,
|
||||
schema: datasetObject.schema,
|
||||
table_name: datasetObject.table_name,
|
||||
};
|
||||
createResource(data).then(response => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
if (typeof response === 'number') {
|
||||
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
|
||||
// When a dataset is created the response we get is its ID number
|
||||
goToPreviousUrl();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={cancelButtonOnClick}>Cancel</Button>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
disabled={!datasetObject?.table_name}
|
||||
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('Create Dataset')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withToasts(Footer);
|
||||
|
@ -31,11 +31,13 @@ import { TableOption } from 'src/components/TableSelector';
|
||||
import RefreshLabel from 'src/components/RefreshLabel';
|
||||
import { Table } from 'src/hooks/apiResources';
|
||||
import Loading from 'src/components/Loading';
|
||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||
import DatabaseSelector, {
|
||||
DatabaseObject,
|
||||
} from 'src/components/DatabaseSelector';
|
||||
import { debounce } from 'lodash';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { DatasetActionType, DatasetObject } from '../types';
|
||||
import { DatasetActionType } from '../types';
|
||||
|
||||
interface LeftPanelProps {
|
||||
setDataset: Dispatch<SetStateAction<object>>;
|
||||
@ -60,7 +62,7 @@ const LeftPanelStyle = styled.div`
|
||||
}
|
||||
.refresh {
|
||||
position: absolute;
|
||||
top: ${theme.gridUnit * 43.25}px;
|
||||
top: ${theme.gridUnit * 37.25}px;
|
||||
left: ${theme.gridUnit * 16.75}px;
|
||||
span[role="button"]{
|
||||
font-size: ${theme.gridUnit * 4.25}px;
|
||||
@ -80,17 +82,28 @@ const LeftPanelStyle = styled.div`
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: ${theme.gridUnit * 97.5}px;
|
||||
top: ${theme.gridUnit * 92.25}px;
|
||||
left: ${theme.gridUnit * 3.25}px;
|
||||
right: 0;
|
||||
.options {
|
||||
cursor: pointer;
|
||||
padding: ${theme.gridUnit * 1.75}px;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
:hover {
|
||||
background-color: ${theme.colors.grayscale.light4}
|
||||
}
|
||||
}
|
||||
.options-highlighted {
|
||||
cursor: pointer;
|
||||
padding: ${theme.gridUnit * 1.75}px;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
background-color: ${theme.colors.primary.dark1};
|
||||
color: ${theme.colors.grayscale.light5};
|
||||
}
|
||||
}
|
||||
form > span[aria-label="refresh"] {
|
||||
position: absolute;
|
||||
top: ${theme.gridUnit * 73}px;
|
||||
top: ${theme.gridUnit * 67.5}px;
|
||||
left: ${theme.gridUnit * 42.75}px;
|
||||
font-size: ${theme.gridUnit * 4.25}px;
|
||||
}
|
||||
@ -108,8 +121,7 @@ const LeftPanelStyle = styled.div`
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
color: ${theme.colors.grayscale.light1}
|
||||
}
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
}
|
||||
}
|
||||
`}
|
||||
@ -125,14 +137,24 @@ export default function LeftPanel({
|
||||
const [loadTables, setLoadTables] = useState(false);
|
||||
const [searchVal, setSearchVal] = useState('');
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [selectedTable, setSelectedTable] = useState<number | null>(null);
|
||||
|
||||
const { addDangerToast } = useToasts();
|
||||
|
||||
const setDatabase = (db: Partial<DatasetObject>) => {
|
||||
setDataset({ type: DatasetActionType.selectDatabase, payload: db });
|
||||
const setDatabase = (db: Partial<DatabaseObject>) => {
|
||||
setDataset({ type: DatasetActionType.selectDatabase, payload: { db } });
|
||||
setSelectedTable(null);
|
||||
setResetTables(true);
|
||||
};
|
||||
|
||||
const setTable = (tableName: string, index: number) => {
|
||||
setSelectedTable(index);
|
||||
setDataset({
|
||||
type: DatasetActionType.selectTable,
|
||||
payload: { name: 'table_name', value: tableName },
|
||||
});
|
||||
};
|
||||
|
||||
const getTablesList = (url: string) => {
|
||||
SupersetClient.get({ url })
|
||||
.then(({ json }) => {
|
||||
@ -164,6 +186,7 @@ export default function LeftPanel({
|
||||
});
|
||||
setLoadTables(true);
|
||||
}
|
||||
setSelectedTable(null);
|
||||
setResetTables(true);
|
||||
};
|
||||
|
||||
@ -212,7 +235,6 @@ export default function LeftPanel({
|
||||
onSchemaChange={setSchema}
|
||||
/>
|
||||
{loadTables && !refresh && Loader('Table loading')}
|
||||
|
||||
{schema && !loadTables && !tableOptions.length && !searchVal && (
|
||||
<div className="emptystate">
|
||||
<EmptyStateMedium
|
||||
@ -245,14 +267,23 @@ export default function LeftPanel({
|
||||
}}
|
||||
className="table-form"
|
||||
placeholder={t('Search tables')}
|
||||
allowClear
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
<div className="options-list" data-test="options-list">
|
||||
{!refresh &&
|
||||
tableOptions.map((o, i) => (
|
||||
<div className="options" key={i}>
|
||||
{o.label}
|
||||
tableOptions.map((option, i) => (
|
||||
<div
|
||||
className={
|
||||
selectedTable === i ? 'options-highlighted' : 'options'
|
||||
}
|
||||
key={i}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setTable(option.value, i)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ export function datasetReducer(
|
||||
case DatasetActionType.selectTable:
|
||||
return {
|
||||
...trimmedState,
|
||||
...action.payload,
|
||||
[action.payload.name]: action.payload.value,
|
||||
};
|
||||
case DatasetActionType.changeDataset:
|
||||
return {
|
||||
@ -78,16 +78,22 @@ export default function AddDataset() {
|
||||
<LeftPanel
|
||||
setDataset={setDataset}
|
||||
schema={dataset?.schema}
|
||||
dbId={dataset?.id}
|
||||
dbId={dataset?.db?.id}
|
||||
/>
|
||||
);
|
||||
const prevUrl =
|
||||
'/tablemodelview/list/?pageIndex=0&sortColumn=changed_on_delta_humanized&sortOrder=desc';
|
||||
|
||||
const FooterComponent = () => (
|
||||
<Footer url={prevUrl} datasetObject={dataset} />
|
||||
);
|
||||
|
||||
return (
|
||||
<DatasetLayout
|
||||
header={HeaderComponent()}
|
||||
leftPanel={LeftPanelComponent()}
|
||||
datasetPanel={DatasetPanel()}
|
||||
footer={Footer()}
|
||||
footer={FooterComponent()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -24,9 +24,11 @@ export enum DatasetActionType {
|
||||
}
|
||||
|
||||
export interface DatasetObject {
|
||||
db: {
|
||||
id: number;
|
||||
database_name?: string;
|
||||
owners?: number[];
|
||||
};
|
||||
schema?: string | null;
|
||||
dataset_name: string;
|
||||
table_name?: string | null;
|
||||
@ -43,10 +45,13 @@ export type Schema = {
|
||||
|
||||
export type DSReducerActionType =
|
||||
| {
|
||||
type: DatasetActionType.selectDatabase | DatasetActionType.selectTable;
|
||||
type: DatasetActionType.selectDatabase;
|
||||
payload: Partial<DatasetObject>;
|
||||
}
|
||||
| {
|
||||
type: DatasetActionType.changeDataset | DatasetActionType.selectSchema;
|
||||
type:
|
||||
| DatasetActionType.changeDataset
|
||||
| DatasetActionType.selectSchema
|
||||
| DatasetActionType.selectTable;
|
||||
payload: DatasetReducerPayloadType;
|
||||
};
|
||||
|
@ -79,8 +79,8 @@ describe('DatasetLayout', () => {
|
||||
});
|
||||
|
||||
it('renders a Footer when passed in', () => {
|
||||
render(<DatasetLayout footer={Footer()} />);
|
||||
render(<DatasetLayout footer={<Footer url="" />} />, { useRedux: true });
|
||||
|
||||
expect(screen.getByText(/footer/i)).toBeVisible();
|
||||
expect(screen.getByText(/Cancel/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -95,6 +95,11 @@ export const StyledLayoutFooter = styled.div`
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
color: ${({ theme }) => theme.colors.info.base};
|
||||
border-top: ${({ theme }) => theme.gridUnit / 4}px solid
|
||||
${({ theme }) => theme.colors.grayscale.light2};
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export const HeaderComponentStyles = styled.div`
|
||||
|
Loading…
Reference in New Issue
Block a user