1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads
2020-12-14 01:04:15 -07:00
parent 6195e888de
commit 81c7ce878a
13 changed files with 239 additions and 154 deletions

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { Flex, Icon, Text } from '@chakra-ui/react'; import { Flex, Icon, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table'; import { usePagination, useSortBy, useTable } from 'react-table';
@@ -68,18 +69,18 @@ export function Table(props: TTable) {
const instance = useTable<TRoute>(options, ...plugins); const instance = useTable<TRoute>(options, ...plugins);
const { const {
getTableProps,
headerGroups,
prepareRow,
page, page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage, gotoPage,
nextPage, nextPage,
previousPage, pageCount,
prepareRow,
canNextPage,
pageOptions,
setPageSize, setPageSize,
headerGroups,
previousPage,
getTableProps,
canPreviousPage,
state: { pageIndex, pageSize }, state: { pageIndex, pageSize },
} = instance; } = instance;
@@ -116,7 +117,6 @@ export function Table(props: TTable) {
<TableBody> <TableBody>
{page.map((row, key) => { {page.map((row, key) => {
prepareRow(row); prepareRow(row);
return ( return (
<TableRow <TableRow
index={key} index={key}
@@ -131,7 +131,8 @@ export function Table(props: TTable) {
align={cell.column.align} align={cell.column.align}
bordersVertical={[bordersVertical, i]} bordersVertical={[bordersVertical, i]}
{...cell.getCellProps()}> {...cell.getCellProps()}>
{cellRender ?? cell.render('Cell')} {/* {cellRender ?? cell.render('Cell')} */}
{React.createElement(cellRender, cell)}
</TableCell> </TableCell>
); );
})} })}

View File

@@ -5,6 +5,10 @@ import type { TCell } from './types';
export const Cell = (props: TCell) => { export const Cell = (props: TCell) => {
const { data, rawData } = props; const { data, rawData } = props;
const cellId = data.column.id as keyof TRoute; const cellId = data.column.id as keyof TRoute;
console.group(cellId);
console.dir(data);
console.dir(rawData);
console.groupEnd();
const component = { const component = {
med: <MonoField v={data.value} />, med: <MonoField v={data.value} />,
age: <Age inSeconds={data.value} />, age: <Age inSeconds={data.value} />,

View File

@@ -1,14 +1,9 @@
import dynamic from 'next/dynamic'; import { Icon, Text, Box, Tooltip, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { import { CgMoreO as More } from '@meronex/icons/cg';
Icon, import { BisError as Warning } from '@meronex/icons/bi';
Text, import { MdNotInterested as NotAllowed, MdLastPage } from '@meronex/icons/md';
Popover, import { BsQuestionCircleFill as Question } from '@meronex/icons/bs';
Tooltip, import { FaCheckCircle as Check, FaChevronRight as ChevronRight } from '@meronex/icons/fa';
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react';
import { MdLastPage } from '@meronex/icons/md';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime'; import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc'; import utcPlugin from 'dayjs/plugin/utc';
@@ -28,19 +23,6 @@ import type {
dayjs.extend(relativeTimePlugin); dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin); dayjs.extend(utcPlugin);
const Check = dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaCheckCircle));
const More = dynamic<MeronexIcon>(() => import('@meronex/icons/cg').then(i => i.CgMoreO));
const NotAllowed = dynamic<MeronexIcon>(() =>
import('@meronex/icons/md').then(i => i.MdNotInterested),
);
const Question = dynamic<MeronexIcon>(() =>
import('@meronex/icons/bs').then(i => i.BsQuestionCircleFill),
);
const Warning = dynamic<MeronexIcon>(() => import('@meronex/icons/bi').then(i => i.BisError));
const ChevronRight = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fa').then(i => i.FaChevronRight),
);
export const MonoField = (props: TMonoField) => { export const MonoField = (props: TMonoField) => {
const { v, ...rest } = props; const { v, ...rest } = props;
return ( return (
@@ -128,22 +110,21 @@ export const Communities = (props: TCommunities) => {
</Tooltip> </Tooltip>
</If> </If>
<If c={communities.length !== 0}> <If c={communities.length !== 0}>
<Popover trigger="hover" placement="right"> <Menu>
<PopoverTrigger> <MenuButton>
<Icon as={More} /> <Icon as={More} />
</PopoverTrigger> </MenuButton>
<PopoverContent <MenuList
p={4} p={3}
width="unset" width="unset"
color={color} color={color}
textAlign="left" textAlign="left"
fontFamily="mono" fontFamily="mono"
fontWeight="normal" fontWeight="normal"
whiteSpace="pre-wrap"> whiteSpace="pre-wrap">
<PopoverArrow />
{communities.join('\n')} {communities.join('\n')}
</PopoverContent> </MenuList>
</Popover> </Menu>
</If> </If>
</> </>
); );
@@ -173,7 +154,7 @@ export const RPKIState = (props: TRPKIState) => {
return ( return (
<Tooltip hasArrow placement="right" label={text[state] ?? text[3]}> <Tooltip hasArrow placement="right" label={text[state] ?? text[3]}>
<Icon icon={icon[state]} color={color[+active][state]} /> <Box as={icon[state]} color={color[+active][state]} />
</Tooltip> </Tooltip>
); );
}; };

View File

@@ -0,0 +1,11 @@
export function isStackError(error: any): error is Error {
return error !== null && 'message' in error;
}
export function isFetchError(error: any): error is Response {
return error !== null && 'statusText' in error;
}
export function isLGError(error: any): error is TQueryResponse {
return error !== null && 'output' in error;
}

View File

@@ -1,5 +1,5 @@
import { forwardRef, useMemo } from 'react'; import { useMemo } from 'react';
import { AccordionIcon, Box, Spinner, Stack, Text, Tooltip } from '@chakra-ui/react'; import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react';
import { BisError as Warning } from '@meronex/icons/bi'; import { BisError as Warning } from '@meronex/icons/bi';
import { FaCheckCircle as Check } from '@meronex/icons/fa'; import { FaCheckCircle as Check } from '@meronex/icons/fa';
import { useConfig, useColorValue } from '~/context'; import { useConfig, useColorValue } from '~/context';
@@ -15,8 +15,8 @@ const runtimeText = (runtime: number, text: string): string => {
return `${text} ${unit}`; return `${text} ${unit}`;
}; };
export const ResultHeader = forwardRef<HTMLDivElement, TResultHeader>((props, ref) => { export const ResultHeader = (props: TResultHeader) => {
const { title, loading, error, errorMsg, errorLevel, runtime } = props; const { title, loading, isError, errorMsg, errorLevel, runtime } = props;
const status = useColorValue('primary.500', 'primary.300'); const status = useColorValue('primary.500', 'primary.300');
const warning = useColorValue(`${errorLevel}.500`, `${errorLevel}.300`); const warning = useColorValue(`${errorLevel}.500`, `${errorLevel}.300`);
@@ -27,20 +27,27 @@ export const ResultHeader = forwardRef<HTMLDivElement, TResultHeader>((props, re
const label = useMemo(() => runtimeText(runtime, text), [runtime]); const label = useMemo(() => runtimeText(runtime, text), [runtime]);
return ( return (
<Stack ref={ref} isInline alignItems="center" w="100%"> <HStack alignItems="center" w="100%">
<Tooltip
hasArrow
placement="top"
isDisabled={loading}
label={isError ? errorMsg : label}
colorScheme={isError ? errorLevel : 'success'}>
{loading ? ( {loading ? (
<Spinner size="sm" mr={4} color={status} /> <Spinner size="sm" mr={4} color={status} />
) : error ? (
<Tooltip hasArrow label={errorMsg} placement="top">
<Box as={Warning} color={warning} mr={4} boxSize={6} />
</Tooltip>
) : ( ) : (
<Tooltip hasArrow label={label} placement="top"> <Box
<Box as={Check} color={defaultStatus} mr={4} boxSize={6} /> as={isError ? Warning : Check}
</Tooltip> color={isError ? warning : defaultStatus}
mr={4}
boxSize={6}
/>
)} )}
</Tooltip>
<Text fontSize="lg">{title}</Text> <Text fontSize="lg">{title}</Text>
<AccordionIcon ml="auto" /> <AccordionIcon ml="auto" />
</Stack> </HStack>
); );
}); };

View File

@@ -10,17 +10,16 @@ import {
AccordionButton, AccordionButton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { BsLightningFill } from '@meronex/icons/bs'; import { BsLightningFill } from '@meronex/icons/bs';
import useAxios from 'axios-hooks';
import { startCase } from 'lodash'; import { startCase } from 'lodash';
import { BGPTable, Countdown, CopyButton, RequeryButton, TextOutput, If } from '~/components'; import { BGPTable, Countdown, CopyButton, RequeryButton, TextOutput, If } from '~/components';
import { useColorValue, useConfig, useMobile } from '~/context'; import { useColorValue, useConfig, useMobile } from '~/context';
import { useStrf, useTableToString } from '~/hooks'; import { useStrf, useLGQuery, useTableToString } from '~/hooks';
import { isStructuredOutput, isStringOutput } from '~/types';
import { FormattedError } from './error'; import { FormattedError } from './error';
import { ResultHeader } from './header'; import { ResultHeader } from './header';
import { isStackError, isFetchError, isLGError } from './guards';
import type { TAccordionHeaderWrapper, TResult } from './types'; import type { TAccordionHeaderWrapper, TResult, TErrorLevels } from './types';
type TErrorLevels = 'success' | 'warning' | 'error';
const AccordionHeaderWrapper = (props: TAccordionHeaderWrapper) => { const AccordionHeaderWrapper = (props: TAccordionHeaderWrapper) => {
const { hoverBg, ...rest } = props; const { hoverBg, ...rest } = props;
@@ -38,7 +37,6 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
const { const {
index, index,
device, device,
timeout,
queryVrf, queryVrf,
queryType, queryType,
queryTarget, queryTarget,
@@ -54,20 +52,12 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400'); const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50'); const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
let [{ data, loading, error }, refetch] = useAxios( const { data, error, isError, isLoading, refetch } = useLGQuery({
{ queryLocation,
url: '/api/query/', queryTarget,
method: 'post', queryType,
data: { queryVrf,
query_vrf: queryVrf, });
query_type: queryType,
query_target: queryTarget,
query_location: queryLocation,
},
timeout,
},
{ useCache: false },
);
const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]); const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]);
@@ -79,16 +69,23 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
setOverride(true); setOverride(true);
}; };
const errorKw = (error && error.response?.data?.keywords) || []; const errorKeywords = useMemo(() => {
let kw = [] as string[];
if (isLGError(error)) {
kw = error.keywords;
}
return kw;
}, [isError]);
let errorMsg; let errorMsg;
if (error && error.response?.data?.output) {
errorMsg = error.response.data.output; if (isLGError(error)) {
} else if (error && error.message.startsWith('timeout')) { errorMsg = error.output as string;
} else if (isFetchError(error)) {
errorMsg = startCase(error.statusText);
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
errorMsg = messages.request_timeout; errorMsg = messages.request_timeout;
} else if (error?.response?.statusText) { } else if (isStackError(error)) {
errorMsg = startCase(error.response.statusText);
} else if (error && error.message) {
errorMsg = startCase(error.message); errorMsg = startCase(error.message);
} else { } else {
errorMsg = messages.general; errorMsg = messages.general;
@@ -96,7 +93,7 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
error && console.error(error); error && console.error(error);
const getErrorLevel = (): TErrorLevels => { const errorLevel = useMemo<TErrorLevels>(() => {
const statusMap = { const statusMap = {
success: 'success', success: 'success',
warning: 'warning', warning: 'warning',
@@ -106,18 +103,22 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
let e: TErrorLevels = 'error'; let e: TErrorLevels = 'error';
if (error?.response?.data?.level) { if (isLGError(error)) {
const idx = error.response.data.level as TResponseLevel; const idx = error.level as TResponseLevel;
e = statusMap[idx]; e = statusMap[idx];
} }
return e; return e;
}; }, [error]);
const errorLevel = useMemo(() => getErrorLevel(), [error]); const tableComponent = useMemo<boolean>(() => {
let result = false;
if (typeof queryType.match(/^bgp_\w+$/) !== null && data?.format === 'application/json') {
result = true;
}
return result;
}, [queryType, data?.format]);
const tableComponent = useMemo(() => typeof queryType.match(/^bgp_\w+$/) !== null, [queryType]); let copyValue = data?.output as string;
let copyValue = data?.output;
const formatData = useTableToString(queryTarget, data, [data?.format]); const formatData = useTableToString(queryTarget, data, [data?.format]);
@@ -130,18 +131,22 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
} }
useEffect(() => { useEffect(() => {
!loading && resultsComplete === null && setComplete(index); if (isLoading && resultsComplete === null) {
}, [loading, resultsComplete]); setComplete(index);
}
}, [isLoading, resultsComplete]);
useEffect(() => { useEffect(() => {
resultsComplete === index && !hasOverride && setOpen(true); if (resultsComplete === index && !hasOverride) {
setOpen(true);
}
}, [resultsComplete, index]); }, [resultsComplete, index]);
return ( return (
<AccordionItem <AccordionItem
ref={ref} ref={ref}
isOpen={isOpen} isOpen={isOpen}
isDisabled={loading} isDisabled={isLoading}
css={{ css={{
'&:last-of-type': { borderBottom: 'none' }, '&:last-of-type': { borderBottom: 'none' },
'&:first-of-type': { borderTop: 'none' }, '&:first-of-type': { borderTop: 'none' },
@@ -155,17 +160,17 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
flex="1 0 auto" flex="1 0 auto"
onClick={handleToggle}> onClick={handleToggle}>
<ResultHeader <ResultHeader
error={error} isError={isError}
loading={loading} loading={isLoading}
errorMsg={errorMsg} errorMsg={errorMsg}
errorLevel={errorLevel} errorLevel={errorLevel}
runtime={data?.runtime} runtime={data?.runtime ?? 0}
title={device.display_name} title={device.display_name}
/> />
</AccordionButton> </AccordionButton>
<ButtonGroup px={[1, 1, 3, 3]} py={2}> <ButtonGroup px={[1, 1, 3, 3]} py={2}>
<CopyButton copyValue={copyValue} isDisabled={loading} /> <CopyButton copyValue={copyValue} isDisabled={isLoading} />
<RequeryButton requery={refetch} variant="ghost" isDisabled={loading} /> <RequeryButton requery={refetch} variant="ghost" isDisabled={isLoading} />
</ButtonGroup> </ButtonGroup>
</AccordionHeaderWrapper> </AccordionHeaderWrapper>
<AccordionPanel <AccordionPanel
@@ -188,20 +193,16 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
}}> }}>
<Flex direction="column" flexWrap="wrap"> <Flex direction="column" flexWrap="wrap">
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}> <Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}>
<If c={!error && data}> {!isError && typeof data !== 'undefined' && (
<If c={tableComponent}> <>
<BGPTable>{data?.output}</BGPTable> {isStructuredOutput(data) && tableComponent ? (
</If> <BGPTable>{data.output}</BGPTable>
<If c={!tableComponent}> ) : isStringOutput(data) && !tableComponent ? (
<TextOutput>{data?.output}</TextOutput> <TextOutput>{data.output}</TextOutput>
</If> ) : null}
</If> </>
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
</Alert>
)} )}
{isError && <Alert rounded="lg" my={2} py={4} status={errorLevel}></Alert>}
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -1,10 +1,10 @@
import type { BoxProps, FlexProps } from '@chakra-ui/react'; import type { BoxProps, FlexProps } from '@chakra-ui/react';
import type { TDevice, TQueryTypes } from '~/types'; import type { TDevice, TQueryTypes, TFormState } from '~/types';
export interface TResultHeader { export interface TResultHeader {
title: string; title: string;
loading: boolean; loading: boolean;
error?: Error; isError?: boolean;
errorMsg: string; errorMsg: string;
errorLevel: 'success' | 'warning' | 'error'; errorLevel: 'success' | 'warning' | 'error';
runtime: number; runtime: number;
@@ -22,18 +22,14 @@ export interface TAccordionHeaderWrapper extends FlexProps {
export interface TResult { export interface TResult {
index: number; index: number;
device: TDevice; device: TDevice;
timeout: number;
queryVrf: string; queryVrf: string;
queryType: TQueryTypes; queryType: TQueryTypes;
queryTarget: string; queryTarget: string;
setComplete(v: number | null): void; setComplete(v: number | null): void;
queryLocation: string; queryLocation: string[];
resultsComplete: number | null; resultsComplete: number | null;
} }
export interface TResults extends BoxProps { export type TResults = TFormState & BoxProps;
queryType: TQueryTypes;
queryLocation: string[]; export type TErrorLevels = 'success' | 'warning' | 'error';
queryTarget: string;
queryVrf: string;
}

View File

@@ -1,6 +1,7 @@
export * from './useBooleanValue'; export * from './useBooleanValue';
export * from './useDevice'; export * from './useDevice';
export * from './useGreeting'; export * from './useGreeting';
export * from './useLGQuery';
export * from './useOpposingColor'; export * from './useOpposingColor';
export * from './useSessionStorage'; export * from './useSessionStorage';
export * from './useStrf'; export * from './useStrf';

View File

@@ -3,8 +3,4 @@ export interface TOpposingOptions {
dark?: string; dark?: string;
} }
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
output: TStructuredResponse;
}
export type TUseGreetingReturn = [boolean, (v?: boolean) => void]; export type TUseGreetingReturn = [boolean, (v?: boolean) => void];

View File

@@ -0,0 +1,62 @@
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import type { TFormState } from '~/types';
/**
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
*
* Adapted from: https://lowmess.com/blog/fetch-with-timeout
*/
export async function fetchWithTimeout(
uri: string,
options: RequestInit = {},
timeout: number,
controller: AbortController,
): Promise<Response> {
/**
* Lets set up our `AbortController`, and create a request options object that includes the
* controller's `signal` to pass to `fetch`.
*/
const { signal = new AbortController().signal, ...allOptions } = options;
const config = { ...allOptions, signal };
/**
* Set a timeout limit for the request using `setTimeout`. If the body of this timeout is
* reached before the request is completed, it will be cancelled.
*/
setTimeout(() => {
controller.abort();
}, timeout);
return await fetch(uri, config);
}
export function useLGQuery(query: TFormState) {
const { request_timeout } = useConfig();
const controller = new AbortController();
async function runQuery(url: string, requestData: TFormState): Promise<TQueryResponse> {
const { queryLocation, queryTarget, queryType, queryVrf } = requestData;
const res = await fetchWithTimeout(
url,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query_location: queryLocation,
query_target: queryTarget,
query_type: queryType,
query_vrf: queryVrf,
}),
mode: 'cors',
},
request_timeout * 1000,
controller,
);
return await res.json();
}
return useQuery<TQueryResponse, Response | TQueryResponse | Error>(
['/api/query/', query],
runQuery,
{ refetchInterval: false },
);
}

View File

@@ -3,8 +3,7 @@ import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime'; import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc'; import utcPlugin from 'dayjs/plugin/utc';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { isStructuredOutput } from '~/types';
import { TStringTableData } from './types';
dayjs.extend(relativeTimePlugin); dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin); dayjs.extend(utcPlugin);
@@ -48,10 +47,10 @@ function formatTime(val: number): string {
export function useTableToString( export function useTableToString(
target: string, target: string,
data: TStringTableData, data: TQueryResponse | undefined,
...deps: any ...deps: any
): () => string { ): () => string {
const { web, parsed_data_fields } = useConfig(); const { web, parsed_data_fields, messages } = useConfig();
function formatRpkiState(val: number): string { function formatRpkiState(val: number): string {
const rpkiStates = [ const rpkiStates = [
@@ -83,12 +82,13 @@ export function useTableToString(
} }
} }
function doFormat(target: string, data: TStringTableData): string { function doFormat(target: string, data: TQueryResponse | undefined): string {
let result = messages.no_output;
try { try {
if (typeof data !== 'undefined' && isStructuredOutput(data)) {
let tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`]; let tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`];
for (const route of data.output.routes) {
data.output.routes.map(route => { for (const field of parsed_data_fields) {
parsed_data_fields.map(field => {
const [header, accessor, align] = field; const [header, accessor, align] = field;
if (align !== null) { if (align !== null) {
let value = route[accessor]; let value = route[accessor];
@@ -100,9 +100,11 @@ export function useTableToString(
tableStringParts.push(` - ${header}: ${value}`); tableStringParts.push(` - ${header}: ${value}`);
} }
} }
}); }
}); }
return tableStringParts.join('\n'); result = tableStringParts.join('\n');
}
return result;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return `An error occurred while parsing the output: '${err.message}'`; return `An error occurred while parsing the output: '${err.message}'`;

View File

@@ -7,3 +7,18 @@ export interface TFormData {
query_vrf: string; query_vrf: string;
query_target: string; query_target: string;
} }
export interface TFormState {
queryLocation: string[];
queryType: TQueryTypes;
queryVrf: string;
queryTarget: string;
}
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
output: TStructuredResponse;
}
export interface TQueryResponseString extends Omit<TQueryResponse, 'output'> {
output: string;
}

View File

@@ -1,4 +1,4 @@
import { TValidQueryTypes } from './data'; import { TValidQueryTypes, TStringTableData, TQueryResponseString } from './data';
export function isQueryType(q: any): q is TValidQueryTypes { export function isQueryType(q: any): q is TValidQueryTypes {
let result = false; let result = false;
@@ -14,3 +14,11 @@ export function isQueryType(q: any): q is TValidQueryTypes {
export function isString(a: any): a is string { export function isString(a: any): a is string {
return typeof a === 'string'; return typeof a === 'string';
} }
export function isStructuredOutput(data: any): data is TStringTableData {
return typeof data.output !== 'string';
}
export function isStringOutput(data: any): data is TQueryResponseString {
return typeof data.output === 'string';
}