Skip to content

Commit

Permalink
Merge pull request #8092 from tinyspeck/sarabee-vtadmin-vtexplain
Browse files Browse the repository at this point in the history
[vtadmin-web] The hastiest-ever VTExplain UI
  • Loading branch information
ajm188 authored May 11, 2021
2 parents 6fa00f9 + cfc52a8 commit 705202f
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
16 changes: 16 additions & 0 deletions web/vtadmin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,19 @@ export const fetchWorkflow = async (params: { clusterID: string; keyspace: strin

return pb.Workflow.create(result);
};

export const fetchVTExplain = async <R extends pb.IVTExplainRequest>({ cluster, keyspace, sql }: R) => {
// As an easy enhancement for later, we can also validate the request parameters on the front-end
// instead of defaulting to '', to save a round trip.
const req = new URLSearchParams();
req.append('cluster', cluster || '');
req.append('keyspace', keyspace || '');
req.append('sql', sql || '');

const { result } = await vtfetch(`/api/vtexplain?${req}`);

const err = pb.VTExplainResponse.verify(result);
if (err) throw Error(err);

return pb.VTExplainResponse.create(result);
};
5 changes: 5 additions & 0 deletions web/vtadmin/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Schema } from './routes/Schema';
import { Stream } from './routes/Stream';
import { Workflows } from './routes/Workflows';
import { Workflow } from './routes/Workflow';
import { VTExplain } from './routes/VTExplain';

export const App = () => {
return (
Expand Down Expand Up @@ -64,6 +65,10 @@ export const App = () => {
<Tablets />
</Route>

<Route path="/vtexplain">
<VTExplain />
</Route>

<Route path="/workflows">
<Workflows />
</Route>
Expand Down
57 changes: 57 additions & 0 deletions web/vtadmin/src/components/routes/VTExplain.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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.
*/
.panel,
.errorPanel {
border: solid 1px var(--colorScaffoldingHighlight);
border-radius: 6px;
height: auto;
max-width: 960px;
padding: 2.4rem;
}

.errorPanel {
border-color: var(--colorError50);
overflow: auto;
}

.container {
display: grid;
grid-gap: 16px;
max-width: 720px;
}

.form {
display: grid;
grid-gap: 8px;
}

.sqlInput {
display: block;
font-family: var(--fontFamilyMonospace);
line-height: var(--lineHeightBody);
padding: 0.8rem;
resize: vertical;
width: 100%;
}

.codeContainer {
max-width: 100%;
overflow: auto;
}

.buttons {
margin: 1.6rem 0 0.8rem 0;
}
123 changes: 123 additions & 0 deletions web/vtadmin/src/components/routes/VTExplain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* 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 React from 'react';
import { orderBy } from 'lodash-es';

import { vtadmin as pb } from '../../proto/vtadmin';
import { useKeyspaces, useVTExplain } from '../../hooks/api';
import { Button } from '../Button';
import { Select } from '../inputs/Select';
import { ContentContainer } from '../layout/ContentContainer';
import { WorkspaceHeader } from '../layout/WorkspaceHeader';
import { WorkspaceTitle } from '../layout/WorkspaceTitle';
import style from './VTExplain.module.scss';
import { Code } from '../Code';
import { useDocumentTitle } from '../../hooks/useDocumentTitle';
import { Label } from '../inputs/Label';

// TODO(doeg): persist form data in URL, error handling, loading state, um... hm. Most things still need doing.
// This whole component is the hastiest prototype ever. :')
export const VTExplain = () => {
useDocumentTitle('VTExplain');

const { data: keyspaces = [] } = useKeyspaces();

const [clusterID, updateCluster] = React.useState<string | null | undefined>(null);
const [keyspaceName, updateKeyspace] = React.useState<string | null | undefined>(null);
const [sql, updateSQL] = React.useState<string | null | undefined>(null);

const selectedKeyspace =
clusterID && keyspaceName
? keyspaces?.find((k) => k.cluster?.id === clusterID && k.keyspace?.name === keyspaceName)
: null;

const { data, error, refetch } = useVTExplain(
{ cluster: clusterID, keyspace: keyspaceName, sql },
{
// Never cache, never refetch.
cacheTime: 0,
enabled: false,
refetchOnWindowFocus: false,
retry: false,
}
);

const onChangeKeyspace = (selectedKeyspace: pb.Keyspace | null | undefined) => {
updateCluster(selectedKeyspace?.cluster?.id);
updateKeyspace(selectedKeyspace?.keyspace?.name);
};

const onChangeSQL: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
updateSQL(e.target.value);
};

const onSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
refetch();
};

return (
<div>
<WorkspaceHeader>
<WorkspaceTitle>VTExplain</WorkspaceTitle>
</WorkspaceHeader>
<ContentContainer className={style.container}>
<section className={style.panel}>
<form className={style.form} onSubmit={onSubmit}>
<div>
<Select
itemToString={(keyspace) => keyspace?.keyspace?.name || ''}
items={orderBy(keyspaces, ['keyspace.name', 'cluster.id'])}
label="Keyspace"
onChange={onChangeKeyspace}
placeholder="Choose a keyspace"
renderItem={(keyspace) => `${keyspace?.keyspace?.name} (${keyspace?.cluster?.id})`}
selectedItem={selectedKeyspace || null}
/>
</div>
<div>
<Label label="SQL">
<textarea
className={style.sqlInput}
onChange={onChangeSQL}
rows={10}
value={sql || ''}
/>
</Label>
</div>
<div className={style.buttons}>
<Button type="submit">Run VTExplain</Button>
</div>
</form>
</section>

{error && (
<section className={style.errorPanel}>
<Code code={JSON.stringify(error, null, 2)} />
</section>
)}

{data?.response && (
<section className={style.panel}>
<div className={style.codeContainer}>
<Code code={data?.response} />
</div>
</section>
)}
</ContentContainer>
</div>
);
};
8 changes: 8 additions & 0 deletions web/vtadmin/src/hooks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
fetchTablets,
fetchVSchema,
FetchVSchemaParams,
fetchVTExplain,
fetchWorkflow,
fetchWorkflows,
} from '../api/http';
Expand Down Expand Up @@ -114,6 +115,13 @@ export const useVSchema = (params: FetchVSchemaParams, options?: UseQueryOptions
return useQuery(['vschema', params], () => fetchVSchema(params));
};

export const useVTExplain = (
params: Parameters<typeof fetchVTExplain>[0],
options?: UseQueryOptions<pb.VTExplainResponse, Error> | undefined
) => {
return useQuery(['vtexplain', params], () => fetchVTExplain(params), { ...options });
};

/**
* useWorkflow is a query hook that fetches a single workflow for the given parameters.
*/
Expand Down

0 comments on commit 705202f

Please sign in to comment.