Skip to content

Commit

Permalink
Merge pull request #7440 from tinyspeck/sarabee-vtadmin-web-hooks
Browse files Browse the repository at this point in the history
[vtadmin-web] Add routes and simple tables for all entities
  • Loading branch information
rohit-nayak-ps authored Feb 4, 2021
2 parents 77fd148 + 8ef5878 commit 4689c0f
Show file tree
Hide file tree
Showing 16 changed files with 79,454 additions and 6,403 deletions.
20 changes: 20 additions & 0 deletions web/vtadmin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions web/vtadmin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@types/react-dom": "^16.9.10",
"@types/react-router-dom": "^5.1.7",
"classnames": "^2.2.6",
"lodash-es": "^4.17.20",
"node-sass": "^4.14.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
Expand Down Expand Up @@ -59,6 +60,7 @@
]
},
"devDependencies": {
"@types/lodash-es": "^4.17.4",
"msw": "^0.24.4",
"prettier": "^2.2.1",
"protobufjs": "^6.10.2",
Expand Down
81 changes: 21 additions & 60 deletions web/vtadmin/src/api/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,22 @@ describe('api/http', () => {
expect(result).toEqual(response);
});

it('parses and returns JSON, given an HttpErrorResponse response', async () => {
it('throws an error if response.ok is false', async () => {
const endpoint = `/api/tablets`;
const response = { ok: false };
mockServerJson(endpoint, response);

const result = await api.vtfetch(endpoint);
expect(result).toEqual(response);
expect.assertions(3);

try {
await api.fetchTablets();
} catch (e) {
/* eslint-disable jest/no-conditional-expect */
expect(e.name).toEqual(HTTP_RESPONSE_NOT_OK_ERROR);
expect(e.message).toEqual(endpoint);
expect(e.response).toEqual(response);
/* eslint-enable jest/no-conditional-expect */
}
});

it('throws an error on malformed JSON', async () => {
Expand Down Expand Up @@ -186,70 +195,22 @@ describe('api/http', () => {
});
});

describe('fetchTablets', () => {
it('returns a list of Tablets, given a successful response', async () => {
const t0 = pb.Tablet.create({ tablet: { hostname: 't0' } });
const t1 = pb.Tablet.create({ tablet: { hostname: 't1' } });
const t2 = pb.Tablet.create({ tablet: { hostname: 't2' } });
const tablets = [t0, t1, t2];

mockServerJson(`/api/tablets`, {
ok: true,
result: {
tablets: tablets.map((t) => t.toJSON()),
},
});

const result = await api.fetchTablets();
expect(result).toEqual(tablets);
});

it('throws an error if response.ok is false', async () => {
const response = { ok: false };
mockServerJson('/api/tablets', response);

expect.assertions(3);

try {
await api.fetchTablets();
} catch (e) {
/* eslint-disable jest/no-conditional-expect */
expect(e.name).toEqual(HTTP_RESPONSE_NOT_OK_ERROR);
expect(e.message).toEqual('/api/tablets');
expect(e.response).toEqual(response);
/* eslint-enable jest/no-conditional-expect */
}
});

describe('vtfetchEntities', () => {
it('throws an error if result.tablets is not an array', async () => {
mockServerJson('/api/tablets', { ok: true, result: { tablets: null } });

expect.assertions(1);

try {
await api.fetchTablets();
} catch (e) {
/* eslint-disable jest/no-conditional-expect */
expect(e.message).toMatch('expected tablets to be an array');
/* eslint-enable jest/no-conditional-expect */
}
});

it('throws an error if JSON cannot be unmarshalled into Tablet objects', async () => {
mockServerJson(`/api/tablets`, {
ok: true,
result: {
tablets: [{ cluster: 'this should be an object, not a string' }],
},
});
const endpoint = '/api/foos';
mockServerJson(endpoint, { ok: true, result: { foos: null } });

expect.assertions(1);

try {
await api.fetchTablets();
await api.vtfetchEntities({
endpoint,
extract: (res) => res.result.foos,
transform: (e) => null, // doesn't matter
});
} catch (e) {
/* eslint-disable jest/no-conditional-expect */
expect(e.message).toEqual('cluster.object expected');
expect(e.message).toMatch('expected entities to be an array, got null');
/* eslint-enable jest/no-conditional-expect */
}
});
Expand Down
85 changes: 74 additions & 11 deletions web/vtadmin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,84 @@ export const vtfetchOpts = (): RequestInit => {
return { credentials };
};

export const fetchTablets = async () => {
const endpoint = '/api/tablets';
const res = await vtfetch(endpoint);
// vtfetchEntities is a helper function for querying vtadmin-api endpoints
// that return a list of protobuf entities.
export const vtfetchEntities = async <T>(opts: {
endpoint: string;
// Extract the list of entities from the response. We can't (strictly)
// guarantee type safety for API responses, hence the `any` return type.
extract: (res: HttpOkResponse) => any;
// Transform an individual entity in the array to its (proto)typed form.
// This will almost always be a `.verify` followed by a `.create`,
// but because of how protobufjs structures its generated types,
// writing this in a generic way is... unpleasant, and difficult to read.
transform: (e: object) => T;
}): Promise<T[]> => {
const res = await vtfetch(opts.endpoint);

// Throw "not ok" responses so that react-query correctly interprets them as errors.
// See https://react-query.tanstack.com/guides/query-functions#handling-and-throwing-errors
if (!res.ok) throw new HttpResponseNotOkError(endpoint, res);
if (!res.ok) throw new HttpResponseNotOkError(opts.endpoint, res);

const tablets = res.result?.tablets;
if (!Array.isArray(tablets)) throw Error(`expected tablets to be an array, got ${tablets}`);
const entities = opts.extract(res);
if (!Array.isArray(entities)) {
throw Error(`expected entities to be an array, got ${entities}`);
}

return tablets.map((t: any) => {
const err = pb.Tablet.verify(t);
if (err) throw Error(err);
return entities.map(opts.transform);
};

return pb.Tablet.create(t);
export const fetchClusters = async () =>
vtfetchEntities({
endpoint: '/api/clusters',
extract: (res) => res.result.clusters,
transform: (e) => {
const err = pb.Cluster.verify(e);
if (err) throw Error(err);
return pb.Cluster.create(e);
},
});

export const fetchGates = async () =>
vtfetchEntities({
endpoint: '/api/gates',
extract: (res) => res.result.gates,
transform: (e) => {
const err = pb.VTGate.verify(e);
if (err) throw Error(err);
return pb.VTGate.create(e);
},
});

export const fetchKeyspaces = async () =>
vtfetchEntities({
endpoint: '/api/keyspaces',
extract: (res) => res.result.keyspaces,
transform: (e) => {
const err = pb.Keyspace.verify(e);
if (err) throw Error(err);
return pb.Keyspace.create(e);
},
});

export const fetchSchemas = async () =>
vtfetchEntities({
endpoint: '/api/schemas',
extract: (res) => res.result.schemas,
transform: (e) => {
const err = pb.Schema.verify(e);
if (err) throw Error(err);
return pb.Schema.create(e);
},
});

export const fetchTablets = async () =>
vtfetchEntities({
endpoint: '/api/tablets',
extract: (res) => res.result.tablets,
transform: (e) => {
const err = pb.Tablet.verify(e);
if (err) throw Error(err);
return pb.Tablet.create(e);
},
});
};
20 changes: 20 additions & 0 deletions web/vtadmin/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { Tablets } from './routes/Tablets';
import { Debug } from './routes/Debug';
import { NavRail } from './NavRail';
import { Error404 } from './routes/Error404';
import { Clusters } from './routes/Clusters';
import { Gates } from './routes/Gates';
import { Keyspaces } from './routes/Keyspaces';
import { Schemas } from './routes/Schemas';

export const App = () => {
return (
Expand All @@ -32,6 +36,22 @@ export const App = () => {

<div className={style.mainContainer}>
<Switch>
<Route path="/clusters">
<Clusters />
</Route>

<Route path="/gates">
<Gates />
</Route>

<Route path="/keyspaces">
<Keyspaces />
</Route>

<Route path="/schemas">
<Schemas />
</Route>

<Route path="/tablets">
<Tablets />
</Route>
Expand Down
18 changes: 11 additions & 7 deletions web/vtadmin/src/components/NavRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ import { Link, NavLink } from 'react-router-dom';

import style from './NavRail.module.scss';
import logo from '../img/vitess-icon-color.svg';
import { useTablets } from '../hooks/api';
import { useClusters, useGates, useKeyspaces, useTableDefinitions, useTablets } from '../hooks/api';
import { Icon, Icons } from './Icon';

export const NavRail = () => {
const { data: tabletData } = useTablets();
const { data: clusters = [] } = useClusters();
const { data: keyspaces = [] } = useKeyspaces();
const { data: gates = [] } = useGates();
const { data: schemas = [] } = useTableDefinitions();
const { data: tablets = [] } = useTablets();

return (
<div className={style.container}>
Expand All @@ -43,19 +47,19 @@ export const NavRail = () => {
<ul className={style.navList}>
<li>
{/* FIXME replace this with a C when we have one */}
<NavRailLink icon={Icons.keyR} text="Clusters" to="/clusters" count={0} />
<NavRailLink icon={Icons.keyR} text="Clusters" to="/clusters" count={clusters.length} />
</li>
<li>
<NavRailLink icon={Icons.keyG} text="Gates" to="/gates" count={0} />
<NavRailLink icon={Icons.keyG} text="Gates" to="/gates" count={gates.length} />
</li>
<li>
<NavRailLink icon={Icons.keyK} text="Keyspaces" to="/keyspaces" count={0} />
<NavRailLink icon={Icons.keyK} text="Keyspaces" to="/keyspaces" count={keyspaces.length} />
</li>
<li>
<NavRailLink icon={Icons.keyS} text="Schemas" to="/schemas" count={0} />
<NavRailLink icon={Icons.keyS} text="Schemas" to="/schemas" count={schemas.length} />
</li>
<li>
<NavRailLink icon={Icons.keyT} text="Tablets" to="/tablets" count={(tabletData || []).length} />
<NavRailLink icon={Icons.keyT} text="Tablets" to="/tablets" count={tablets.length} />
</li>
</ul>

Expand Down
50 changes: 0 additions & 50 deletions web/vtadmin/src/components/TabletList.tsx

This file was deleted.

Loading

0 comments on commit 4689c0f

Please sign in to comment.