From c0b96449c0f2aa51910458587e69a6202d5510ef Mon Sep 17 00:00:00 2001
From: Sara Bee <855595+doeg@users.noreply.github.com>
Date: Fri, 30 Apr 2021 13:18:25 -0400
Subject: [PATCH] [vtadmin-web] Display timestamps + stream counts on Workflows
view
Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com>
---
web/vtadmin/package-lock.json | 5 ++
web/vtadmin/package.json | 1 +
.../src/components/pips/StreamStatePip.tsx | 33 +++++++++
.../components/routes/Workflows.module.scss | 7 ++
.../src/components/routes/Workflows.tsx | 61 +++++++++++-----
web/vtadmin/src/index.css | 4 ++
web/vtadmin/src/util/time.ts | 42 +++++++++++
web/vtadmin/src/util/workflows.test.ts | 72 +++++++++++++++++++
web/vtadmin/src/util/workflows.ts | 43 +++++++++++
9 files changed, 250 insertions(+), 18 deletions(-)
create mode 100644 web/vtadmin/src/components/pips/StreamStatePip.tsx
create mode 100644 web/vtadmin/src/util/time.ts
create mode 100644 web/vtadmin/src/util/workflows.test.ts
create mode 100644 web/vtadmin/src/util/workflows.ts
diff --git a/web/vtadmin/package-lock.json b/web/vtadmin/package-lock.json
index 3e44e8a070c..375bec857a8 100644
--- a/web/vtadmin/package-lock.json
+++ b/web/vtadmin/package-lock.json
@@ -5133,6 +5133,11 @@
"whatwg-url": "^8.0.0"
}
},
+ "dayjs": {
+ "version": "1.10.4",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
+ "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw=="
+ },
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
diff --git a/web/vtadmin/package.json b/web/vtadmin/package.json
index 12ec97abd53..bb0b453e730 100644
--- a/web/vtadmin/package.json
+++ b/web/vtadmin/package.json
@@ -15,6 +15,7 @@
"@types/react-dom": "^16.9.10",
"@types/react-router-dom": "^5.1.7",
"classnames": "^2.2.6",
+ "dayjs": "^1.10.4",
"downshift": "^6.1.0",
"history": "^5.0.0",
"lodash-es": "^4.17.20",
diff --git a/web/vtadmin/src/components/pips/StreamStatePip.tsx b/web/vtadmin/src/components/pips/StreamStatePip.tsx
new file mode 100644
index 00000000000..e68d358b654
--- /dev/null
+++ b/web/vtadmin/src/components/pips/StreamStatePip.tsx
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2021 The Vitess Authors.
+ *
+ * Licensed 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 { Pip, PipState } from './Pip';
+
+interface Props {
+ state?: string | null | undefined;
+}
+
+// TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340)
+const STREAM_STATES: { [key: string]: PipState } = {
+ Copying: 'primary',
+ Error: 'danger',
+ Running: 'success',
+ Stopped: null,
+};
+
+export const StreamStatePip = ({ state }: Props) => {
+ const pipState = state ? STREAM_STATES[state] : null;
+ return ;
+};
diff --git a/web/vtadmin/src/components/routes/Workflows.module.scss b/web/vtadmin/src/components/routes/Workflows.module.scss
index c6cd7194d4b..aa05570c364 100644
--- a/web/vtadmin/src/components/routes/Workflows.module.scss
+++ b/web/vtadmin/src/components/routes/Workflows.module.scss
@@ -19,3 +19,10 @@
grid-template-columns: 1fr min-content;
margin-bottom: 24px;
}
+
+.shardList {
+ color: var(--textColorSecondary);
+ font-size: var(--fontSizeSmall);
+ max-width: 20rem;
+ padding-right: 24px;
+}
diff --git a/web/vtadmin/src/components/routes/Workflows.tsx b/web/vtadmin/src/components/routes/Workflows.tsx
index 14777459f05..cc6beb8514a 100644
--- a/web/vtadmin/src/components/routes/Workflows.tsx
+++ b/web/vtadmin/src/components/routes/Workflows.tsx
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { orderBy } from 'lodash-es';
+import { groupBy, orderBy } from 'lodash-es';
import * as React from 'react';
import { Link } from 'react-router-dom';
@@ -27,22 +27,27 @@ import { Icons } from '../Icon';
import { TextInput } from '../TextInput';
import { useSyncedURLParam } from '../../hooks/useSyncedURLParam';
import { filterNouns } from '../../util/filterNouns';
+import { getStreams, getTimeUpdated } from '../../util/workflows';
+import { formatDateTime, formatRelativeTime } from '../../util/time';
+import { StreamStatePip } from '../pips/StreamStatePip';
export const Workflows = () => {
useDocumentTitle('Workflows');
- const { data } = useWorkflows();
+ const { data } = useWorkflows({ refetchInterval: 1000 });
const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
const sortedData = React.useMemo(() => {
- const mapped = (data || []).map(({ cluster, keyspace, workflow }) => ({
- clusterID: cluster?.id,
- clusterName: cluster?.name,
- keyspace,
- name: workflow?.name,
- source: workflow?.source?.keyspace,
- sourceShards: workflow?.source?.shards,
- target: workflow?.target?.keyspace,
- targetShards: workflow?.target?.shards,
+ const mapped = (data || []).map((workflow) => ({
+ clusterID: workflow.cluster?.id,
+ clusterName: workflow.cluster?.name,
+ keyspace: workflow.keyspace,
+ name: workflow.workflow?.name,
+ source: workflow.workflow?.source?.keyspace,
+ sourceShards: workflow.workflow?.source?.shards,
+ streams: groupBy(getStreams(workflow), 'state'),
+ target: workflow.workflow?.target?.keyspace,
+ targetShards: workflow.workflow?.target?.shards,
+ timeUpdated: getTimeUpdated(workflow),
}));
const filtered = filterNouns(filter, mapped);
return orderBy(filtered, ['name', 'clusterName', 'source', 'target']);
@@ -65,9 +70,7 @@ export const Workflows = () => {
{row.source ? (
<>
{row.source}
-
- {(row.sourceShards || []).join(', ')}
-
+ {(row.sourceShards || []).join(', ')}
>
) : (
N/A
@@ -77,14 +80,32 @@ export const Workflows = () => {
{row.target ? (
<>
{row.target}
-
- {(row.targetShards || []).join(', ')}
-
+ {(row.targetShards || []).join(', ')}
>
) : (
N/A
)}
+
+ {/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */}
+ {['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => (
+
+ {streamState in row.streams ? (
+ <>
+ {row.streams[streamState].length}
+ >
+ ) : (
+ -
+ )}
+
+ ))}
+
+
+ {formatDateTime(row.timeUpdated)}
+
+ {formatRelativeTime(row.timeUpdated)}
+
+
);
});
@@ -106,7 +127,11 @@ export const Workflows = () => {
-
+
);
};
diff --git a/web/vtadmin/src/index.css b/web/vtadmin/src/index.css
index 52c453b0a4d..ff81e4dd254 100644
--- a/web/vtadmin/src/index.css
+++ b/web/vtadmin/src/index.css
@@ -203,6 +203,10 @@ table tbody td[rowSpan] {
font-family: var(--fontFamilyMonospace);
}
+.font-family-primary {
+ font-family: var(--fontFamilyPrimary);
+}
+
.font-size-small {
font-size: var(--fontSizeSmall);
}
diff --git a/web/vtadmin/src/util/time.ts b/web/vtadmin/src/util/time.ts
new file mode 100644
index 00000000000..ab51e47fb5c
--- /dev/null
+++ b/web/vtadmin/src/util/time.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2021 The Vitess Authors.
+ *
+ * Licensed 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 * as dayjs from 'dayjs';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(localizedFormat);
+dayjs.extend(relativeTime);
+
+export const parse = (timestamp: number | null | undefined): dayjs.Dayjs | null => {
+ if (typeof timestamp !== 'number') {
+ return null;
+ }
+ return dayjs.unix(timestamp);
+};
+
+export const format = (timestamp: number | null | undefined, template: string | undefined): string | null => {
+ const u = parse(timestamp);
+ return u ? u.format(template) : null;
+};
+
+export const formatDateTime = (timestamp: number | null | undefined): string | null => {
+ return format(timestamp, 'YYYY-MM-DD LT');
+};
+
+export const formatRelativeTime = (timestamp: number | null | undefined): string | null => {
+ const u = parse(timestamp);
+ return u ? u.fromNow() : null;
+};
diff --git a/web/vtadmin/src/util/workflows.test.ts b/web/vtadmin/src/util/workflows.test.ts
new file mode 100644
index 00000000000..38dc0e31987
--- /dev/null
+++ b/web/vtadmin/src/util/workflows.test.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2021 The Vitess Authors.
+ *
+ * Licensed 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 { vtadmin as pb } from '../proto/vtadmin';
+import { getStreams } from './workflows';
+
+describe('getStreams', () => {
+ const tests: {
+ name: string;
+ input: Parameters;
+ expected: ReturnType;
+ }[] = [
+ {
+ name: 'should return a flat list of streams',
+ input: [
+ pb.Workflow.create({
+ workflow: {
+ shard_streams: {
+ '-80/us_east_1a-123456': {
+ streams: [
+ { id: 1, shard: '-80' },
+ { id: 2, shard: '-80' },
+ ],
+ },
+ '80-/us_east_1a-789012': {
+ streams: [
+ { id: 1, shard: '80-' },
+ { id: 2, shard: '80-' },
+ ],
+ },
+ },
+ },
+ }),
+ ],
+ expected: [
+ { id: 1, shard: '-80' },
+ { id: 2, shard: '-80' },
+ { id: 1, shard: '80-' },
+ { id: 2, shard: '80-' },
+ ],
+ },
+ {
+ name: 'should handle when shard streams undefined',
+ input: [pb.Workflow.create()],
+ expected: [],
+ },
+ {
+ name: 'should handle null input',
+ input: [null],
+ expected: [],
+ },
+ ];
+
+ test.each(tests.map(Object.values))(
+ '%s',
+ (name: string, input: Parameters, expected: ReturnType) => {
+ expect(getStreams(...input)).toEqual(expected);
+ }
+ );
+});
diff --git a/web/vtadmin/src/util/workflows.ts b/web/vtadmin/src/util/workflows.ts
new file mode 100644
index 00000000000..99b85b07efa
--- /dev/null
+++ b/web/vtadmin/src/util/workflows.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2021 The Vitess Authors.
+ *
+ * Licensed 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 { vtctldata, vtadmin as pb } from '../proto/vtadmin';
+
+/**
+ * getStreams returns a flat list of streams across all keyspaces/shards in the workflow.
+ */
+export const getStreams = (workflow: W | null | undefined): vtctldata.Workflow.IStream[] => {
+ if (!workflow) {
+ return [];
+ }
+
+ return Object.values(workflow.workflow?.shard_streams || {}).reduce((acc, shardStream) => {
+ (shardStream.streams || []).forEach((stream) => {
+ acc.push(stream);
+ });
+ return acc;
+ }, [] as vtctldata.Workflow.IStream[]);
+};
+
+/**
+ * getTimeUpdated returns the `time_updated` timestamp of the most recently
+ * updated stream in the workflow.
+ */
+export const getTimeUpdated = (workflow: W | null | undefined): number => {
+ // Note: long-term it may be better to get this from the `vreplication_log` data
+ // added by https://github.com/vitessio/vitess/pull/7831
+ const timestamps = getStreams(workflow).map((s) => parseInt(`${s.time_updated?.seconds}`, 10));
+ return Math.max(...timestamps);
+};