Web console: show stages for MSQ compaction tasks (#18545)

* show stages for MSQ compaction tasks

* more robust
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index 1342c53..849cc47 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -51,6 +51,7 @@
 export * from './rule-editor/rule-editor';
 export * from './segment-timeline/segment-timeline';
 export * from './show-json/show-json';
+export * from './show-json-or-stages/show-json-or-stages';
 export * from './show-log/show-log';
 export * from './show-value/show-value';
 export * from './splitter-layout/splitter-layout';
diff --git a/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap b/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
new file mode 100644
index 0000000..052bb7e
--- /dev/null
+++ b/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShowJsonOrStages matches snapshot 1`] = `
+<div
+  class="show-json-or-stages"
+>
+  <div
+    class="top-actions"
+  >
+    <div
+      class="bp5-button-group right-buttons"
+    >
+      <button
+        class="bp5-button bp5-disabled bp5-minimal"
+        disabled=""
+        tabindex="-1"
+        type="button"
+      >
+        <span
+          class="bp5-button-text"
+        >
+          Download
+        </span>
+      </button>
+      <button
+        class="bp5-button bp5-disabled bp5-minimal"
+        disabled=""
+        tabindex="-1"
+        type="button"
+      >
+        <span
+          class="bp5-button-text"
+        >
+          Copy
+        </span>
+      </button>
+      <button
+        class="bp5-button bp5-disabled bp5-minimal"
+        disabled=""
+        tabindex="-1"
+        type="button"
+      >
+        <span
+          class="bp5-button-text"
+        >
+          View raw
+        </span>
+      </button>
+    </div>
+  </div>
+  <div
+    class="main-area"
+  >
+    <div
+      class="loader"
+    >
+      <div
+        class="loader-logo"
+      >
+        <svg
+          viewBox="0 0 100 100"
+        >
+          <path
+            class="one"
+            d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
+          c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
+          c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
+          />
+          <path
+            class="two"
+            d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
+            c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
+            C63.5,58,59.9,59.5,55.7,59.5z"
+          />
+          <path
+            class="three"
+            d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
+          />
+          <path
+            class="four"
+            d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
+            C46.4,69.2,45.8,69.8,45.1,69.8z"
+          />
+        </svg>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-console/src/components/show-json-or-stages/show-json-or-stages.scss b/web-console/src/components/show-json-or-stages/show-json-or-stages.scss
new file mode 100644
index 0000000..f4c7c60
--- /dev/null
+++ b/web-console/src/components/show-json-or-stages/show-json-or-stages.scss
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+.show-json-or-stages {
+  position: relative;
+  height: 100%;
+
+  .top-actions {
+    text-align: right;
+    padding-bottom: 10px;
+
+    & > * {
+      display: inline-block;
+    }
+  }
+
+  .main-area {
+    position: absolute;
+    width: 100%;
+    top: 40px;
+    bottom: 0;
+
+    textarea {
+      height: 100%;
+      width: 100%;
+      resize: none;
+    }
+
+    .loader {
+      position: relative;
+    }
+
+    .execution-stages-pane {
+      height: 100%;
+    }
+  }
+}
diff --git a/web-console/src/components/show-json-or-stages/show-json-or-stages.spec.tsx b/web-console/src/components/show-json-or-stages/show-json-or-stages.spec.tsx
new file mode 100644
index 0000000..32660e6
--- /dev/null
+++ b/web-console/src/components/show-json-or-stages/show-json-or-stages.spec.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 { render } from '@testing-library/react';
+
+import { ShowJsonOrStages } from './show-json-or-stages';
+
+describe('ShowJsonOrStages', () => {
+  it('matches snapshot', () => {
+    const showJsonOrStages = <ShowJsonOrStages endpoint="test" downloadFilename="test" />;
+    const { container } = render(showJsonOrStages);
+    expect(container.firstChild).toMatchSnapshot();
+  });
+});
diff --git a/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx b/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
new file mode 100644
index 0000000..8010104
--- /dev/null
+++ b/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { Button, ButtonGroup, Intent } from '@blueprintjs/core';
+import copy from 'copy-to-clipboard';
+import * as JSONBig from 'json-bigint-native';
+import React from 'react';
+import AceEditor from 'react-ace';
+
+import { Execution } from '../../druid-models';
+import { useQueryManager } from '../../hooks';
+import { Api, AppToaster, UrlBaser } from '../../singletons';
+import { downloadFile } from '../../utils';
+import { ExecutionStagesPane } from '../../views/workbench-view/execution-stages-pane/execution-stages-pane';
+import { Loader } from '../loader/loader';
+
+import './show-json-or-stages.scss';
+
+export interface ShowJsonOrStagesProps {
+  endpoint: string;
+  transform?: (x: any) => any;
+  downloadFilename?: string;
+}
+
+export const ShowJsonOrStages = React.memo(function ShowJsonOrStages(props: ShowJsonOrStagesProps) {
+  const { endpoint, transform, downloadFilename } = props;
+
+  const [jsonState] = useQueryManager<null, [string, Execution | undefined]>({
+    processQuery: async (_, signal) => {
+      const resp = await Api.instance.get(endpoint, { signal });
+      let data = resp.data;
+      if (transform) data = transform(data);
+
+      let execution: Execution | undefined;
+      if (data.multiStageQuery) {
+        try {
+          execution = Execution.fromTaskReport(data);
+        } catch (e) {
+          console.error(`Could not parse task report as MSQ execution: ${e.message}`);
+        }
+      }
+
+      return [
+        typeof data === 'string' ? data : JSONBig.stringify(data, undefined, 2),
+        execution,
+      ] as [string, Execution | undefined];
+    },
+    initQuery: null,
+  });
+
+  const [jsonValue, execution] = jsonState.data || [''];
+  return (
+    <div className="show-json-or-stages">
+      <div className="top-actions">
+        <ButtonGroup className="right-buttons">
+          {downloadFilename && (
+            <Button
+              disabled={jsonState.loading}
+              text="Download"
+              minimal
+              onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
+            />
+          )}
+          <Button
+            text="Copy"
+            minimal
+            disabled={jsonState.loading}
+            onClick={() => {
+              copy(jsonValue, { format: 'text/plain' });
+              AppToaster.show({
+                message: 'JSON value copied to clipboard',
+                intent: Intent.SUCCESS,
+              });
+            }}
+          />
+          <Button
+            text="View raw"
+            disabled={!jsonValue}
+            minimal
+            onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+          />
+        </ButtonGroup>
+      </div>
+
+      <div className="main-area">
+        {jsonState.loading ? (
+          <Loader />
+        ) : execution ? (
+          <ExecutionStagesPane execution={execution} />
+        ) : (
+          <AceEditor
+            mode="hjson"
+            theme="solarized_dark"
+            readOnly
+            fontSize={12}
+            width="100%"
+            height="100%"
+            showPrintMargin={false}
+            showGutter={false}
+            value={!jsonState.error ? jsonValue : jsonState.getErrorMessage()}
+            style={{}}
+          />
+        )}
+      </div>
+    </div>
+  );
+});
diff --git a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
index 9edc5d9..5638971 100644
--- a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
+++ b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
@@ -18,7 +18,7 @@
 
 import React, { useState } from 'react';
 
-import { ShowJson, ShowLog } from '../../components';
+import { ShowJson, ShowJsonOrStages, ShowLog } from '../../components';
 import { Api } from '../../singletons';
 import { deepGet } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
@@ -83,7 +83,7 @@
         />
       )}
       {activeTab === 'report' && (
-        <ShowJson
+        <ShowJsonOrStages
           endpoint={`${taskEndpointBase}/reports`}
           transform={x => deepGet(x, 'ingestionStatsAndErrors.payload') || x}
           downloadFilename={`task-reports-${taskId}.json`}
diff --git a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
index d7c447d..e4717c6 100644
--- a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
+++ b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
@@ -157,7 +157,7 @@
   execution: Execution;
   onErrorClick?(): void;
   onWarningClick?(): void;
-  goToTask(taskId: string): void;
+  goToTask?(taskId: string): void;
 }
 
 export const ExecutionStagesPane = React.memo(function ExecutionStagesPane(
@@ -245,8 +245,10 @@
             Header: 'Worker',
             id: 'worker',
             accessor: d => d.index,
+            className: goToTask ? undefined : 'padded',
             width: 95,
             Cell({ value }) {
+              if (!goToTask) return `Worker${value}`;
               const taskId = `${execution.id}-worker${value}_0`;
               return (
                 <TableClickableCell