feat: Dataset Creation Footer Component (#21241)

Co-authored-by: lyndsiWilliams <kcatgirl@gmail.com>
This commit is contained in:
AAfghahi 2022-09-23 19:40:10 -04:00 committed by GitHub
parent 4913da1511
commit c4638fa2b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 30 deletions

View File

@ -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,

View File

@ -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);
});

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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,10 +121,9 @@ 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>

View File

@ -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()}
/>
);
}

View File

@ -24,9 +24,11 @@ export enum DatasetActionType {
}
export interface DatasetObject {
id: number;
database_name?: string;
owners?: number[];
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;
};

View File

@ -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();
});
});

View File

@ -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`