Skip to content

Commit

Permalink
Merge pull request #8091 from tinyspeck/sarabee-workflow-details
Browse files Browse the repository at this point in the history
[vtadmin-web] Add initial Stream view, render streams on Workflow view
  • Loading branch information
ajm188 authored May 10, 2021
2 parents 493eafb + 4dc2dd4 commit 6fa00f9
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 19 deletions.
7 changes: 6 additions & 1 deletion web/vtadmin/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Gates } from './routes/Gates';
import { Keyspaces } from './routes/Keyspaces';
import { Schemas } from './routes/Schemas';
import { Schema } from './routes/Schema';
import { Stream } from './routes/Stream';
import { Workflows } from './routes/Workflows';
import { Workflow } from './routes/Workflow';

Expand Down Expand Up @@ -67,10 +68,14 @@ export const App = () => {
<Workflows />
</Route>

<Route path="/workflow/:clusterID/:keyspace/:name">
<Route exact path="/workflow/:clusterID/:keyspace/:name">
<Workflow />
</Route>

<Route path="/workflow/:clusterID/:keyspace/:workflowName/stream/:tabletCell/:tabletUID/:streamID">
<Stream />
</Route>

<Route path="/debug">
<Debug />
</Route>
Expand Down
10 changes: 9 additions & 1 deletion web/vtadmin/src/components/dataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ interface Props<T> {
data: T[];
pageSize?: number;
renderRows: (rows: T[]) => JSX.Element[];
title?: string;
}

// Generally, page sizes of ~100 rows are fine in terms of performance,
// but anything over ~50 feels unwieldy in terms of UX.
const DEFAULT_PAGE_SIZE = 50;

export const DataTable = <T extends object>({ columns, data, pageSize = DEFAULT_PAGE_SIZE, renderRows }: Props<T>) => {
export const DataTable = <T extends object>({
columns,
data,
pageSize = DEFAULT_PAGE_SIZE,
renderRows,
title,
}: Props<T>) => {
const { pathname } = useLocation();
const urlQuery = useURLQuery();

Expand All @@ -58,6 +65,7 @@ export const DataTable = <T extends object>({ columns, data, pageSize = DEFAULT_
return (
<div>
<table>
{title && <caption>{title}</caption>}
<thead>
<tr>
{columns.map((col, cdx) => (
Expand Down
34 changes: 34 additions & 0 deletions web/vtadmin/src/components/routes/Stream.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.
*/
.headingMeta {
display: flex;
}

.headingMeta span {
display: inline-block;
line-height: 2;

&::after {
color: var(--colorScaffoldingHighlight);
content: '/';
display: inline-block;
margin: 0 1.2rem;
}

&:last-child::after {
content: none;
}
}
84 changes: 84 additions & 0 deletions web/vtadmin/src/components/routes/Stream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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 { Link, useParams } from 'react-router-dom';

import { useWorkflow } from '../../hooks/api';
import { useDocumentTitle } from '../../hooks/useDocumentTitle';
import { formatStreamKey, getStreams } from '../../util/workflows';
import { Code } from '../Code';
import { ContentContainer } from '../layout/ContentContainer';
import { NavCrumbs } from '../layout/NavCrumbs';
import { WorkspaceHeader } from '../layout/WorkspaceHeader';
import { WorkspaceTitle } from '../layout/WorkspaceTitle';
import style from './Stream.module.scss';

interface RouteParams {
clusterID: string;
keyspace: string;
streamID: string;
tabletCell: string;
tabletUID: string;
workflowName: string;
}

export const Stream = () => {
const params = useParams<RouteParams>();
const { data: workflow } = useWorkflow(
{
clusterID: params.clusterID,
keyspace: params.keyspace,
name: params.workflowName,
},
{ refetchInterval: 1000 }
);

const streamID = parseInt(params.streamID, 10);
const tabletUID = parseInt(params.tabletUID, 10);
const tabletAlias = { cell: params.tabletCell, uid: tabletUID };
const streamKey = formatStreamKey({ id: streamID, tablet: tabletAlias });

useDocumentTitle(`${streamKey} (${params.workflowName})`);

const stream = getStreams(workflow).find(
(s) => s.id === streamID && s.tablet?.cell === tabletAlias.cell && s.tablet?.uid === tabletAlias.uid
);

return (
<div>
<WorkspaceHeader>
<NavCrumbs>
<Link to="/workflows">Workflows</Link>
<Link to={`/workflow/${params.clusterID}/${params.keyspace}/${params.workflowName}`}>
{params.workflowName}
</Link>
</NavCrumbs>

<WorkspaceTitle className="font-family-monospace">{streamKey}</WorkspaceTitle>
<div className={style.headingMeta}>
<span>
Cluster: <code>{params.clusterID}</code>
</span>
<span>
Target keyspace: <code>{params.keyspace}</code>
</span>
</div>
</WorkspaceHeader>
<ContentContainer>
<Code code={JSON.stringify(stream, null, 2)} />
</ContentContainer>
</div>
);
};
2 changes: 1 addition & 1 deletion web/vtadmin/src/components/routes/Tablets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const formatRows = (
const shard = shardName ? keyspace?.shards[shardName] : null;

return {
alias: formatAlias(t),
alias: formatAlias(t.tablet?.alias),
cluster: t.cluster?.name,
hostname: t.tablet?.hostname,
isShardServing: shard?.shard?.is_master_serving,
Expand Down
38 changes: 38 additions & 0 deletions web/vtadmin/src/components/routes/Workflow.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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.
*/
.headingMeta {
display: flex;
}

.headingMeta span {
display: inline-block;
line-height: 2;

&::after {
color: var(--colorScaffoldingHighlight);
content: '/';
display: inline-block;
margin: 0 1.2rem;
}

&:last-child::after {
content: none;
}
}

.streamTable {
margin: 0 0 48px 0;
}
87 changes: 82 additions & 5 deletions web/vtadmin/src/components/routes/Workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,114 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { groupBy, orderBy } from 'lodash-es';
import { Link, useParams } from 'react-router-dom';

import style from './Workflow.module.scss';
import { useWorkflow } from '../../hooks/api';
import { Code } from '../Code';
import { formatStreamKey, getStreams, getStreamSource, getStreamTarget } from '../../util/workflows';
import { DataCell } from '../dataTable/DataCell';
import { DataTable } from '../dataTable/DataTable';
import { ContentContainer } from '../layout/ContentContainer';
import { NavCrumbs } from '../layout/NavCrumbs';
import { WorkspaceHeader } from '../layout/WorkspaceHeader';
import { WorkspaceTitle } from '../layout/WorkspaceTitle';
import { StreamStatePip } from '../pips/StreamStatePip';
import { formatAlias } from '../../util/tablets';
import { useDocumentTitle } from '../../hooks/useDocumentTitle';
import { formatDateTime } from '../../util/time';

interface RouteParams {
clusterID: string;
keyspace: string;
name: string;
}

const COLUMNS = ['Stream', 'Source', 'Target', 'Tablet'];

export const Workflow = () => {
const { clusterID, keyspace, name } = useParams<RouteParams>();
const { data } = useWorkflow({ clusterID, keyspace, name });
useDocumentTitle(`${name} (${keyspace})`);

const { data } = useWorkflow({ clusterID, keyspace, name }, { refetchInterval: 1000 });

const streams = useMemo(() => {
const rows = getStreams(data).map((stream) => ({
key: formatStreamKey(stream),
...stream,
}));

return orderBy(rows, 'streamKey');
}, [data]);

const streamsByState = groupBy(streams, 'state');

const renderRows = (rows: typeof streams) => {
return rows.map((row) => {
const href =
row.tablet && row.id
? `/workflow/${clusterID}/${keyspace}/${name}/stream/${row.tablet.cell}/${row.tablet.uid}/${row.id}`
: null;

return (
<tr key={row.key}>
<DataCell>
<StreamStatePip state={row.state} />{' '}
<Link className="font-weight-bold" to={href}>
{row.key}
</Link>
<div className="font-size-small text-color-secondary">
Updated {formatDateTime(row.time_updated?.seconds)}
</div>
</DataCell>
<DataCell>{getStreamSource(row) || <span className="text-color-secondary">N/A</span>}</DataCell>
<DataCell>
{getStreamTarget(row, keyspace) || <span className="text-color-secondary">N/A</span>}
</DataCell>
<DataCell>{formatAlias(row.tablet)}</DataCell>
</tr>
);
});
};

// Placeholder
return (
<div>
<WorkspaceHeader>
<NavCrumbs>
<Link to="/workflows">Workflows</Link>
</NavCrumbs>

<WorkspaceTitle>{name}</WorkspaceTitle>
<WorkspaceTitle className="font-family-monospace">{name}</WorkspaceTitle>
<div className={style.headingMeta}>
<span>
Cluster: <code>{clusterID}</code>
</span>
<span>
Target keyspace: <code>{keyspace}</code>
</span>
</div>
</WorkspaceHeader>
<ContentContainer>
<Code code={JSON.stringify(data, null, 2)} />
{/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */}
{['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => {
if (!Array.isArray(streamsByState[streamState])) {
return null;
}

return (
<div className={style.streamTable} key={streamState}>
<DataTable
columns={COLUMNS}
data={streamsByState[streamState]}
// TODO(doeg): make pagination optional in DataTable https://github.com/vitessio/vitess/projects/12#card-60810231
pageSize={1000}
renderRows={renderRows}
title={streamState}
/>
</div>
);
})}
</ContentContainer>
</div>
);
Expand Down
8 changes: 6 additions & 2 deletions web/vtadmin/src/components/routes/Workflows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,18 @@ export const Workflows = () => {
].join(' ');

return (
<Tooltip text={tooltip}>
<Tooltip key={streamState} text={tooltip}>
<span className={style.stream}>
<StreamStatePip state={streamState} /> {streamCount}
</span>
</Tooltip>
);
}
return <span className={style.streamPlaceholder}>-</span>;
return (
<span key={streamState} className={style.streamPlaceholder}>
-
</span>
);
})}
</div>
</DataCell>
Expand Down
13 changes: 11 additions & 2 deletions web/vtadmin/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,23 @@ table {
width: 100%;
}

table caption {
background: var(--backgroundSecondary);
color: var(--textColorPrimary);
font-size: var(--fontSizeDefault);
font-weight: 500;
padding: 1.2rem var(--tableCellPadding) 0.8rem var(--tableCellPadding);
text-align: left;
}

table th {
background: var(--backgroundSecondary);
border: solid 1px var(--backgroundSecondary);
border-bottom-color: var(--tableBorderColor);
color: var(--textColorSecondary);
font-size: var(--fontSizeSmall);
font-size: var(--fontSizeDefault);
font-weight: 500;
padding: 12px var(--tableCellPadding);
padding: 8px var(--tableCellPadding);
text-align: left;
}

Expand Down
4 changes: 2 additions & 2 deletions web/vtadmin/src/util/tablets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const TABLET_TYPES = Object.entries(invertBy(topodata.TabletType)).reduce
/**
* formatAlias formats a tablet.alias object as a single string, The Vitess Way™.
*/
export const formatAlias = <T extends pb.ITablet>(t: T) =>
t.tablet?.alias?.cell && t.tablet?.alias?.uid ? `${t.tablet.alias.cell}-${t.tablet.alias.uid}` : null;
export const formatAlias = <A extends topodata.ITabletAlias>(alias: A | null | undefined) =>
alias?.uid ? `${alias.cell}-${alias.uid}` : null;

export const formatType = (t: pb.Tablet) => t.tablet?.type && TABLET_TYPES[t.tablet?.type];

Expand Down
Loading

0 comments on commit 6fa00f9

Please sign in to comment.