Web-Console: add go to editor button to tasks and supervisors view (#7705)

* add go to editor button to tasks and supervisors

* fix package.json

* remove intent

* quuick fixes

* fixes

* add getsupervisorjson and gettaskjson

* remove space

* remove gotoloaddata

* fixes

* add error handling

* save

* add loader

* remove initspec

* fixup! add loader

* update snapshots

* remove gotoloaddataview form headerbar.spec
This commit is contained in:
mcbrewster 2019-05-24 22:52:26 -07:00 committed by Clint Wylie
parent 2b34e1b710
commit 5f50f357a4
7 changed files with 116 additions and 236 deletions

View File

@ -27,7 +27,6 @@ describe('describe header bar', () => {
<HeaderBar
active={'load-data'}
hideLegacy={false}
goToLoadDataView={() => {}}
/>);
expect(headerBar).toMatchSnapshot();
});

View File

@ -52,7 +52,6 @@ export type HeaderActiveTab = null | 'load-data' | 'query' | 'datasources' | 'se
export interface HeaderBarProps extends React.Props<any> {
active: HeaderActiveTab;
hideLegacy: boolean;
goToLoadDataView: () => void;
}
export interface HeaderBarState {

View File

@ -100,7 +100,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
});
}
private initSpec: any | null;
private supervisorId: string | null;
private taskId: string | null;
private openDialog: string | null;
private datasource: string | null;
@ -151,8 +151,8 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
private resetInitialsWithDelay() {
setTimeout(() => {
this.initSpec = null;
this.taskId = null;
this.supervisorId = null;
this.openDialog = null;
this.datasource = null;
this.onlyUnavailable = null;
@ -161,8 +161,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
}, 50);
}
private goToLoadDataView = (initSpec?: any) => {
if (initSpec) this.initSpec = initSpec;
private goToLoadDataView = (supervisorId?: string, taskId?: string ) => {
if (taskId) this.taskId = taskId;
if (supervisorId) this.supervisorId = supervisorId;
window.location.hash = 'load-data';
this.resetInitialsWithDelay();
}
@ -197,7 +198,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
const { hideLegacy } = this.props;
return <>
<HeaderBar active={active} hideLegacy={hideLegacy} goToLoadDataView={this.goToLoadDataView}/>
<HeaderBar active={active} hideLegacy={hideLegacy}/>
<div className={classNames('view-container', classType)}>{el}</div>
</>;
}
@ -208,7 +209,8 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
}
private wrappedLoadDataView = () => {
return this.wrapInViewContainer('load-data', <LoadDataView initSpec={this.initSpec} goToTask={this.goToTask}/>, 'narrow-pad');
return this.wrapInViewContainer('load-data', <LoadDataView initSupervisorId={this.supervisorId} initTaskId={this.taskId} goToTask={this.goToTask}/>, 'narrow-pad');
}
private wrappedSqlView = () => {

View File

@ -2,211 +2,41 @@
exports[`describe load data view load data view snapshot 1`] = `
<div
className="load-data-view app-view connect"
className="load-data-view app-view init"
>
<div
className="bp3-tabs stage-nav"
className="intro"
>
<div
className="stage-section"
key="Connect and parse raw data"
>
<div
className="stage-nav-l1"
>
Connect and parse raw data
</div>
<Blueprint3.ButtonGroup
className="stage-nav-l2"
>
<Blueprint3.Button
active={true}
className="connect"
icon={false}
key="connect"
onClick={[Function]}
text="Connect"
/>
<Blueprint3.Button
active={false}
className="parser"
icon={false}
key="parser"
onClick={[Function]}
text="Parse data"
/>
<Blueprint3.Button
active={false}
className="timestamp"
icon={false}
key="timestamp"
onClick={[Function]}
text="Parse time"
/>
</Blueprint3.ButtonGroup>
</div>
<div
className="stage-section"
key="Transform and configure schema"
>
<div
className="stage-nav-l1"
>
Transform and configure schema
</div>
<Blueprint3.ButtonGroup
className="stage-nav-l2"
>
<Blueprint3.Button
active={false}
className="transform"
icon={false}
key="transform"
onClick={[Function]}
text="Transform"
/>
<Blueprint3.Button
active={false}
className="filter"
icon={false}
key="filter"
onClick={[Function]}
text="Filter"
/>
<Blueprint3.Button
active={false}
className="schema"
icon={false}
key="schema"
onClick={[Function]}
text="Configure schema"
/>
</Blueprint3.ButtonGroup>
</div>
<div
className="stage-section"
key="Tune parameters"
>
<div
className="stage-nav-l1"
>
Tune parameters
</div>
<Blueprint3.ButtonGroup
className="stage-nav-l2"
>
<Blueprint3.Button
active={false}
className="partition"
icon={false}
key="partition"
onClick={[Function]}
text="Partition"
/>
<Blueprint3.Button
active={false}
className="tuning"
icon={false}
key="tuning"
onClick={[Function]}
text="Tune"
/>
<Blueprint3.Button
active={false}
className="publish"
icon={false}
key="publish"
onClick={[Function]}
text="Publish"
/>
</Blueprint3.ButtonGroup>
</div>
<div
className="stage-section"
key="Verify and submit"
>
<div
className="stage-nav-l1"
>
Verify and submit
</div>
<Blueprint3.ButtonGroup
className="stage-nav-l2"
>
<Blueprint3.Button
active={false}
className="json-spec"
icon="eye-open"
key="json-spec"
onClick={[Function]}
text="Edit JSON spec"
/>
</Blueprint3.ButtonGroup>
</div>
Please specify where your raw data is located
</div>
<div
className="main"
className="cards"
>
<Loader
loading={true}
/>
</div>
<div
className="control"
>
<Blueprint3.Callout
className="intro"
<Blueprint3.Card
elevation={0}
interactive={true}
onClick={[Function]}
>
<p>
Druid ingests raw data and converts it into a custom,
<ExternalLink
href="http://druid.io/docs/latest/design/segments.html"
>
indexed
</ExternalLink>
format that is optimized for analytic queries.
</p>
<p>
To get started, please specify where your raw data is stored and what data you want to ingest.
</p>
<p>
Click "Preview" to look at the sampled raw data.
</p>
</Blueprint3.Callout>
<Blueprint3.FormGroup
label="IO Config"
Other (streaming)
</Blueprint3.Card>
<Blueprint3.Card
elevation={0}
interactive={true}
onClick={[Function]}
>
<JSONInput
height="300px"
onChange={[Function]}
value={
Object {
"type": "test",
}
}
/>
</Blueprint3.FormGroup>
<Blueprint3.Button
disabled={false}
onClick={[Function]}
text="Preview"
/>
Other (batch)
</Blueprint3.Card>
</div>
<div
className="next-bar"
<Blueprint3.Alert
canEscapeKeyCancel={false}
canOutsideClickCancel={false}
confirmButtonText="Close"
icon="warning-sign"
intent="warning"
isOpen={false}
onConfirm={[Function]}
>
<Blueprint3.Button
className="prev"
icon="arrow-left"
onClick={[Function]}
text="Restart"
/>
<Blueprint3.Button
disabled={true}
intent="primary"
onClick={[Function]}
text="Next: Parse data"
/>
</div>
<p />
</Blueprint3.Alert>
</div>
`;

View File

@ -29,7 +29,6 @@ describe('describe load data view', () => {
it('load data view snapshot', () => {
const loadDataView = shallow(
<LoadDataView
initSpec={{dataSchema: {dataSource: 'test', parser:{parseSpec: {format: 'test', dimensionsSpec: {}, timestampSpec: {column: 'test', format: 'test', missingValue: 'test'}, }}}, ioConfig: { type: 'test'}}}
goToTask={(taskId: string | null) => {}}
/>);
expect(loadDataView).toMatchSnapshot();

View File

@ -143,8 +143,8 @@ function getTimestampSpec(headerAndRows: HeaderAndRows | null): TimestampSpec {
return timestampSpecs[0] || getEmptyTimestampSpec();
}
type Stage = 'connect' | 'parser' | 'timestamp' | 'transform' | 'filter' | 'schema' | 'partition' | 'tuning' | 'publish' | 'json-spec';
const STAGES: Stage[] = ['connect', 'parser', 'timestamp', 'transform', 'filter', 'schema', 'partition', 'tuning', 'publish', 'json-spec'];
type Stage = 'connect' | 'parser' | 'timestamp' | 'transform' | 'filter' | 'schema' | 'partition' | 'tuning' | 'publish' | 'json-spec' | 'loading';
const STAGES: Stage[] = ['connect', 'parser', 'timestamp', 'transform', 'filter', 'schema', 'partition', 'tuning', 'publish', 'json-spec', 'loading'];
const SECTIONS: { name: string, stages: Stage[] }[] = [
{ name: 'Connect and parse raw data', stages: ['connect', 'parser', 'timestamp'] },
@ -163,19 +163,20 @@ const VIEW_TITLE: Record<Stage, string> = {
'partition': 'Partition',
'tuning': 'Tune',
'publish': 'Publish',
'json-spec': 'Edit JSON spec'
'json-spec': 'Edit JSON spec',
'loading': 'Loading'
};
export interface LoadDataViewProps extends React.Props<any> {
initSpec: IngestionSpec | null;
goToTask: (taskId: string | null, openDialog?: string) => void;
initSupervisorId?: string | null;
initTaskId?: string | null;
goToTask: (taskId: string | null, supervisor?: string) => void;
}
export interface LoadDataViewState {
stage: Stage;
spec: IngestionSpec;
cacheKey: string | undefined;
// dialogs / modals
showResetConfirm: boolean;
newRollup: boolean | null;
@ -225,9 +226,8 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
constructor(props: LoadDataViewProps) {
super(props);
let spec = props.initSpec || parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
if (!spec || typeof spec !== 'object') spec = {};
this.state = {
stage: 'connect',
spec,
@ -281,9 +281,15 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
componentDidMount(): void {
this.getOverlordModules();
this.updateStage('connect');
if (this.props.initTaskId) {
this.updateStage('loading');
this.getTaskJson();
} else if (this.props.initSupervisorId) {
this.updateStage('loading');
this.getSupervisorJson(); } else this.updateStage('connect');
}
async getOverlordModules() {
let overlordModules: string[];
try {
@ -327,8 +333,7 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
render() {
const { stage, spec } = this.state;
if (!Object.keys(spec).length) {
if (!Object.keys(spec).length && !this.props.initSupervisorId && !this.props.initTaskId) {
return <div className={classNames('load-data-view', 'app-view', 'init')}>
{this.renderInitStage()}
</div>;
@ -350,11 +355,11 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
{stage === 'publish' && this.renderPublishStage()}
{stage === 'json-spec' && this.renderJsonSpecStage()}
{stage === 'loading' && this.renderLoading()}
{this.renderResetConfirm()}
</div>;
}
renderStepNav() {
const { stage } = this.state;
@ -1052,7 +1057,7 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
},
minWidth: timestamp ? 200 : 100,
resizable: !timestamp
};
};
})}
defaultPageSize={50}
showPagination={false}
@ -2348,6 +2353,35 @@ export class LoadDataView extends React.Component<LoadDataViewProps, LoadDataVie
}
// ==================================================================
private getSupervisorJson = async (): Promise<void> => {
try {
const resp = await axios.get(`/druid/indexer/v1/supervisor/${this.props.initSupervisorId}`);
this.updateSpec(resp.data);
this.updateStage('json-spec');
} catch (e) {
AppToaster.show({
message: `Failed to get supervisor spec: ${getDruidErrorMessage(e)}`,
intent: Intent.DANGER
});
}
}
private getTaskJson = async (): Promise<void> => {
try {
const resp = await axios.get(`/druid/indexer/v1/task/${this.props.initTaskId}`);
this.updateSpec(resp.data.payload.spec);
this.updateStage('json-spec');
} catch (e) {
AppToaster.show({
message: `Failed to get task spec: ${getDruidErrorMessage(e)}`,
intent: Intent.DANGER
});
}
}
renderLoading() {
return <Loader loading/>;
}
renderJsonSpecStage() {
const { goToTask } = this.props;

View File

@ -48,7 +48,7 @@ export interface TasksViewProps extends React.Props<any> {
openDialog: string | null;
goToSql: (initSql: string) => void;
goToMiddleManager: (middleManager: string) => void;
goToLoadDataView: () => void;
goToLoadDataView: (supervisorId?: string, taskId?: string) => void;
noSqlMode: boolean;
}
@ -286,9 +286,17 @@ ORDER BY "rank" DESC, "created_time" DESC`);
this.taskQueryManager.rerunLastQuery();
}
private getSupervisorActions(id: string, supervisorSuspended: boolean): BasicAction[] {
return [
{
private getSupervisorActions(id: string, supervisorSuspended: boolean, type: string): BasicAction[] {
const actions: BasicAction[] = [];
if (type === 'kafka' || type === 'kinesis') {
actions.push(
{
icon: IconNames.CLOUD_UPLOAD,
title: 'Open in data loader',
onAction: () => this.props.goToLoadDataView(id)
});
}
actions.push({
icon: IconNames.STEP_BACKWARD,
title: 'Reset',
onAction: () => this.setState({ resetSupervisorId: id })
@ -304,7 +312,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
intent: Intent.DANGER,
onAction: () => this.setState({ terminateSupervisorId: id })
}
];
);
// @ts-ignore
return actions;
}
renderResumeSupervisorAction() {
@ -413,7 +423,6 @@ ORDER BY "rank" DESC, "created_time" DESC`);
renderSupervisorTable() {
const { supervisors, supervisorsLoading, supervisorsError } = this.state;
const { supervisorTableColumnSelectionHandler } = this;
return <>
<ReactTable
data={supervisors || []}
@ -477,8 +486,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
filterable: false,
Cell: row => {
const id = row.value;
const type = row.row.type;
const supervisorSuspended = row.original.spec.suspended;
const supervisorActions = this.getSupervisorActions(id, supervisorSuspended);
const supervisorActions = this.getSupervisorActions(id, supervisorSuspended, type);
const supervisorMenu = basicActionsToMenu(supervisorActions);
return <ActionCell>
@ -510,18 +520,24 @@ ORDER BY "rank" DESC, "created_time" DESC`);
</>;
}
// --------------------------------------
private getTaskActions(id: string, status: string): BasicAction[] {
if (status !== 'RUNNING' && status !== 'WAITING' && status !== 'PENDING') return [];
return [
{
private getTaskActions(id: string, status: string, type: string): BasicAction[] {
const actions: BasicAction[] = [];
if (type === 'index' || type === 'index_parallel') {
actions.push({
icon: IconNames.CLOUD_UPLOAD,
title: 'Open in data loader',
onAction: () => this.props.goToLoadDataView(undefined, id)
});
}
if (status === 'RUNNING' || status === 'WAITING' || status === 'PENDING') {
actions.push({
icon: IconNames.CROSS,
title: 'Kill',
intent: Intent.DANGER,
onAction: () => this.setState({ killTaskId: id })
}
];
onAction: () => this.setState({killTaskId: id})
});
}
return actions;
}
renderKillTaskAction() {
@ -657,8 +673,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Cell: row => {
if (row.aggregated) return '';
const id = row.value;
const type = row.row.type;
const { status } = row.original;
const taskActions = this.getTaskActions(id, status);
const taskActions = this.getTaskActions(id, status, type);
const taskMenu = basicActionsToMenu(taskActions);
return <ActionCell>
@ -688,11 +705,11 @@ ORDER BY "rank" DESC, "created_time" DESC`);
</>;
}
render() {
const { goToSql, goToLoadDataView, noSqlMode } = this.props;
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg, taskTableActionDialogId, taskTableActionDialogActions, supervisorTableActionDialogId, supervisorTableActionDialogActions } = this.state;
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
const submitTaskMenu = <Menu>
<MenuItem
text="Raw JSON task"