+
+
+ {canChartMetric && (
+
+
+
+
+
+
+
+
+ )}
+
+ {status === AnalysisStatus.Pending && (
+
+ {metricName} analysis measurements have not yet begun. Measurement information will appear here when it becomes available.
+
+ )}
+ {status !== AnalysisStatus.Pending && metricResults.transformedMeasurements.length === 0 && (
+
Measurement results for {metricName} cannot be displayed.
+ )}
+ {status !== AnalysisStatus.Pending && metricResults.transformedMeasurements.length > 0 && (
+ <>
+
+ {selectedView === 'chart' && (
+
+ )}
+ {selectedView === 'table' && (
+
+ )}
+ >
+ )}
+
+
+ Pass requirements
+
+ 0}
+ />
+
+ {Array.isArray(metricSpec?.queries) && (
+ <>
+
+
+ {metricSpec.queries.length > 1 ? 'Queries' : 'Query'}
+
+
+ {metricSpec.queries.map((query) => (
+
+ ))}
+ >
+ )}
+
+ );
+};
+
+export default MetricPanel;
diff --git a/ui/src/app/components/analysis-modal/panels/styles.scss b/ui/src/app/components/analysis-modal/panels/styles.scss
new file mode 100644
index 0000000000..b0010c55dd
--- /dev/null
+++ b/ui/src/app/components/analysis-modal/panels/styles.scss
@@ -0,0 +1,61 @@
+@import '../theme/theme.scss';
+
+// Analysis Panel
+
+.analysis-header {
+ margin: $space-unit 0 (3 * $space-unit);
+}
+
+.label {
+ display: block;
+}
+
+// Metric Panel
+
+.metric-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: $space-unit 0;
+}
+
+.legend {
+ display: flex;
+ justify-content: flex-end;
+}
+
+h5.section-title {
+ margin-bottom: 0; // antd override
+}
+
+.query-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $space-unit;
+}
+
+.query-box {
+ :not(:last-child) {
+ margin-bottom: $space-small;
+ }
+
+ :last-child {
+ margin-bottom: $space-large;
+ }
+}
+
+// Common
+
+.summary-section,
+.metric-section {
+ margin-bottom: 3 * $space-unit;
+
+ &.medium-space {
+ margin-bottom: $space-medium;
+ }
+
+ &.top-content {
+ margin-top: $space-unit;
+ }
+}
diff --git a/ui/src/app/components/analysis-modal/panels/summary-panel.tsx b/ui/src/app/components/analysis-modal/panels/summary-panel.tsx
new file mode 100644
index 0000000000..4b2d7bb829
--- /dev/null
+++ b/ui/src/app/components/analysis-modal/panels/summary-panel.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react';
+import * as moment from 'moment';
+import {Typography} from 'antd';
+
+import {AnalysisStatus, FunctionalStatus} from '../types';
+import Header from '../header/header';
+
+import classNames from 'classnames/bind';
+import './styles.scss';
+
+const cx = classNames;
+
+const {Text} = Typography;
+
+const timeRangeFormatter = (start: number, end: number | null) => {
+ const startFormatted = moment(start).format('LLL');
+ if (end === null) {
+ return `${startFormatted} - present`;
+ }
+ const isSameDate = moment(start).isSame(moment(end), 'day');
+ const endFormatted = isSameDate ? moment(end).format('LT') : moment(end).format('LLL');
+ return `${startFormatted} - ${endFormatted}`;
+};
+
+interface SummaryPanelProps {
+ className?: string[] | string;
+ endTime: number | null;
+ images: string[];
+ message?: string;
+ revision: string;
+ startTime: number | null;
+ status: AnalysisStatus;
+ substatus?: FunctionalStatus.ERROR | FunctionalStatus.WARNING;
+ title: string;
+}
+
+const SummaryPanel = ({className, endTime, images, message, revision, startTime, status, substatus, title}: SummaryPanelProps) => (
+
+
+ {images.length > 0 && (
+
+
+ {images.length > 1 ? `Versions` : `Version`}
+
+ {images.join(', ')}
+
+ )}
+
+
+ Revision
+
+ {revision}
+
+ {startTime !== null && (
+
+
+ Run time
+
+ {timeRangeFormatter(startTime, endTime)}
+
+ )}
+ {message && (
+
+
+ Summary
+
+ {message}
+
+ )}
+
+);
+
+export default SummaryPanel;
diff --git a/ui/src/app/components/analysis-modal/query-box/query-box.scss b/ui/src/app/components/analysis-modal/query-box/query-box.scss
new file mode 100644
index 0000000000..c12be636b2
--- /dev/null
+++ b/ui/src/app/components/analysis-modal/query-box/query-box.scss
@@ -0,0 +1,50 @@
+@import '../theme/theme.scss';
+
+.query-box {
+ position: relative;
+ padding: $space-small 48px $space-small $space-small;
+ background-color: $gray-2;
+ border: 1px solid $gray-4;
+ max-height: 70px;
+ overflow: hidden;
+ transition: max-height 0.3s ease-in-out;
+
+ &.can-expand {
+ cursor: pointer;
+ }
+
+ .query {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ overflow: auto hidden;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ text-overflow: ellipsis;
+ line-clamp: 3;
+ -webkit-line-clamp: 3;
+ font-family: $font-family-mono;
+ font-size: 12px;
+ margin-bottom: 0;
+ }
+
+ &.is-expanded {
+ max-height: 500px;
+
+ .query {
+ line-clamp: initial;
+ -webkit-line-clamp: initial;
+ }
+ }
+}
+
+.query-copy-button {
+ position: absolute;
+ font-size: 18px;
+ line-height: 1;
+ top: 8px;
+ right: 6px;
+
+ svg {
+ color: $ant-primary;
+ }
+}
diff --git a/ui/src/app/components/analysis-modal/query-box/query-box.tsx b/ui/src/app/components/analysis-modal/query-box/query-box.tsx
new file mode 100644
index 0000000000..188731c5a8
--- /dev/null
+++ b/ui/src/app/components/analysis-modal/query-box/query-box.tsx
@@ -0,0 +1,42 @@
+import * as React from 'react';
+
+import {Typography} from 'antd';
+
+import classNames from 'classnames';
+import './query-box.scss';
+
+const {Paragraph} = Typography;
+
+interface QueryBoxProps {
+ className?: string[] | string;
+ query: string;
+}
+
+const QueryBox = ({className, query}: QueryBoxProps) => {
+ const queryTextRef = React.useRef