From cfc52a89588f0531f47fe4a87108cd30e5266434 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Mon, 10 May 2021 15:59:38 -0400 Subject: [PATCH] [vtadmin-web] The hastiest-ever VTExplain UI Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- web/vtadmin/src/api/http.ts | 16 +++ web/vtadmin/src/components/App.tsx | 5 + .../components/routes/VTExplain.module.scss | 57 ++++++++ .../src/components/routes/VTExplain.tsx | 123 ++++++++++++++++++ web/vtadmin/src/hooks/api.ts | 8 ++ 5 files changed, 209 insertions(+) create mode 100644 web/vtadmin/src/components/routes/VTExplain.module.scss create mode 100644 web/vtadmin/src/components/routes/VTExplain.tsx diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index 937dd6cf080..4f0701d71b0 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -213,3 +213,19 @@ export const fetchWorkflow = async (params: { clusterID: string; keyspace: strin return pb.Workflow.create(result); }; + +export const fetchVTExplain = async ({ 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); +}; diff --git a/web/vtadmin/src/components/App.tsx b/web/vtadmin/src/components/App.tsx index 11f521528a1..74e2f0b68e4 100644 --- a/web/vtadmin/src/components/App.tsx +++ b/web/vtadmin/src/components/App.tsx @@ -28,6 +28,7 @@ import { Schemas } from './routes/Schemas'; import { Schema } from './routes/Schema'; import { Workflows } from './routes/Workflows'; import { Workflow } from './routes/Workflow'; +import { VTExplain } from './routes/VTExplain'; export const App = () => { return ( @@ -63,6 +64,10 @@ export const App = () => { + + + + diff --git a/web/vtadmin/src/components/routes/VTExplain.module.scss b/web/vtadmin/src/components/routes/VTExplain.module.scss new file mode 100644 index 00000000000..acf9ec57f7f --- /dev/null +++ b/web/vtadmin/src/components/routes/VTExplain.module.scss @@ -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; +} diff --git a/web/vtadmin/src/components/routes/VTExplain.tsx b/web/vtadmin/src/components/routes/VTExplain.tsx new file mode 100644 index 00000000000..f30d1271fd2 --- /dev/null +++ b/web/vtadmin/src/components/routes/VTExplain.tsx @@ -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(null); + const [keyspaceName, updateKeyspace] = React.useState(null); + const [sql, updateSQL] = React.useState(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 = (e) => { + updateSQL(e.target.value); + }; + + const onSubmit: React.FormEventHandler = (e) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+ + VTExplain + + +
+
+
+