Add Grid view for the operations view (#283)
* [REMPL_APOLLO_DEVTOOLS] apollo operations - grid view added --------- Co-authored-by: Jyoti Prakash Sahoo <jysahoo@microsoft.com>
This commit is contained in:
Родитель
39e59f9c11
Коммит
5d7d2620ec
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "Apollo operations - grid view added",
|
||||
"packageName": "@graphitation/rempl-apollo-devtools",
|
||||
"email": "jakubvejr@microsoft.com",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
|
@ -18,8 +18,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.4.11",
|
||||
"@fluentui/react-components": "9.0.0-rc.11",
|
||||
"@fluentui/react-tabster": "^9.0.0-rc.7",
|
||||
"@fluentui/react-components": "^9.0.0-rc.15",
|
||||
"@fluentui/react-data-grid-react-window": "^9.0.0-beta.14",
|
||||
"@fluentui/react-icons": "2.0.166-rc.3",
|
||||
"@fluentui/react-tabster": "^9.0.0-rc.14",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/react": "^17.0.2",
|
||||
|
@ -27,7 +29,7 @@
|
|||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"apollo-inspector": "^1.11.0",
|
||||
"apollo-inspector": "^1.16.3",
|
||||
"eslint-plugin-react": "^7.25.2",
|
||||
"graphiql": "^2.0.9",
|
||||
"hotkeys-js": "^3.8.7",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { createRef, useEffect } from "react";
|
||||
import { dialogStyles } from "./activity-dialog.styles";
|
||||
import { Text, Headline, Button } from "@fluentui/react-components";
|
||||
import { Text, Button, Title1 } from "@fluentui/react-components";
|
||||
import { Dismiss20Regular } from "@fluentui/react-icons";
|
||||
import { RecentActivityRaw, WatchedQuery, Mutation } from "../../types";
|
||||
|
||||
|
@ -27,9 +27,9 @@ export const ActivityDialog = React.memo(
|
|||
}}
|
||||
>
|
||||
<div className={classes.header}>
|
||||
<Headline>{`${value.data?.name} (${
|
||||
<Title1>{`${value.data?.name} (${
|
||||
value.isMutation ? "Mutation" : "Watched Query"
|
||||
})`}</Headline>
|
||||
})`}</Title1>
|
||||
<Button
|
||||
appearance="transparent"
|
||||
ref={closeIcon}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { createRef, useEffect } from "react";
|
||||
import { dialogStyles } from "./dialog.styles";
|
||||
import { Text, Headline, Button } from "@fluentui/react-components";
|
||||
import { Text, Button, Title1 } from "@fluentui/react-components";
|
||||
import { CacheObjectWithSize } from "../../subscriber/apollo-cache/types";
|
||||
import { Dismiss20Regular } from "@fluentui/react-icons";
|
||||
|
||||
|
@ -26,7 +26,7 @@ export const Dialog = React.memo(({ value, onClose }: DialogProps) => {
|
|||
}}
|
||||
>
|
||||
<div className={classes.header}>
|
||||
<Headline className={classes.name}>{value?.key}</Headline>
|
||||
<Title1 className={classes.name}>{value?.key}</Title1>
|
||||
<Button
|
||||
appearance="transparent"
|
||||
ref={closeIcon}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
IStopTracking,
|
||||
IInspectorTrackingConfig,
|
||||
IDataView,
|
||||
IVerboseOperation,
|
||||
} from "apollo-inspector";
|
||||
import { WrapperCallbackParams } from "../../types";
|
||||
|
||||
|
@ -14,6 +15,7 @@ export class ApolloOperationsTrackerPublisher {
|
|||
protected stopTracking: IStopTracking | undefined;
|
||||
protected activeClient: ApolloClient<NormalizedCacheObject> | undefined;
|
||||
protected isRecording: boolean;
|
||||
protected lastDataReceived: Map<number, IVerboseOperation> | null;
|
||||
|
||||
constructor(remplWrapper: RemplWrapper) {
|
||||
this.remplWrapper = remplWrapper;
|
||||
|
@ -25,6 +27,7 @@ export class ApolloOperationsTrackerPublisher {
|
|||
);
|
||||
this.apolloPublisher = remplWrapper.publisher;
|
||||
this.attachMethodsToPublisher();
|
||||
this.lastDataReceived = null;
|
||||
}
|
||||
|
||||
protected attachMethodsToPublisher() {
|
||||
|
@ -50,6 +53,11 @@ export class ApolloOperationsTrackerPublisher {
|
|||
try {
|
||||
const data = this.stopTracking?.();
|
||||
this.publishApolloOperations(data);
|
||||
const operationsMap = new Map();
|
||||
data.verboseOperations?.forEach((op) => {
|
||||
operationsMap.set(op.id, op);
|
||||
});
|
||||
this.lastDataReceived = operationsMap;
|
||||
} catch (error) {
|
||||
// publish error to subscriber to show error UX
|
||||
this.publishApolloOperations({
|
||||
|
@ -62,6 +70,33 @@ export class ApolloOperationsTrackerPublisher {
|
|||
this.stopTracking = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
this.apolloPublisher.provide(
|
||||
"copyOperationsData",
|
||||
(ids: number[] | undefined) => {
|
||||
if (ids && ids.length === 1 && ids[0] === -1) {
|
||||
const stringified = JSON.stringify(
|
||||
(this.activeClient?.cache as any).data.data,
|
||||
);
|
||||
window.navigator.clipboard.writeText(stringified);
|
||||
return;
|
||||
}
|
||||
if (this.lastDataReceived) {
|
||||
const copiedOperations: IVerboseOperation[] = [];
|
||||
ids?.forEach((id) => {
|
||||
if (this.lastDataReceived?.has(id)) {
|
||||
copiedOperations.push(
|
||||
this.lastDataReceived.get(id) as IVerboseOperation,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
copiedOperations.sort((a, b) => a.id - b.id);
|
||||
const stringified = JSON.stringify(copiedOperations);
|
||||
window.navigator.clipboard.writeText(stringified);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected publishApolloOperations(data: IDataView) {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import * as React from "react";
|
||||
import { IAffectedQueryMap, IDueToOperation } from "apollo-inspector";
|
||||
import { AffectedQueriesRenderer } from "./affected-queries-renderer";
|
||||
import { SelectTabData } from "@fluentui/react-components";
|
||||
|
||||
export interface IAffectedQueriesContainerProps {
|
||||
affectedQueries: IAffectedQueryMap | null;
|
||||
}
|
||||
export const AffectedQueriesContainer = (
|
||||
props: IAffectedQueriesContainerProps,
|
||||
) => {
|
||||
const { affectedQueries } = props;
|
||||
const listOfItems = React.useMemo(() => getListOfItems(affectedQueries), [
|
||||
affectedQueries,
|
||||
]);
|
||||
const [selectedItem, setSelectedItem] = React.useState<string>(
|
||||
(listOfItems && listOfItems.length > 0 && listOfItems[0].name) || "",
|
||||
);
|
||||
|
||||
const gridItems = React.useMemo(
|
||||
() => getGridItems(affectedQueries, selectedItem),
|
||||
[affectedQueries, selectedItem],
|
||||
);
|
||||
|
||||
const onTabSelect = React.useCallback(
|
||||
(_, { value }: SelectTabData) => {
|
||||
setSelectedItem(value as string);
|
||||
},
|
||||
[setSelectedItem],
|
||||
);
|
||||
|
||||
return (
|
||||
<AffectedQueriesRenderer
|
||||
listOfItems={listOfItems}
|
||||
gridItems={gridItems}
|
||||
selectedListItem={selectedItem}
|
||||
onTabSelect={onTabSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getGridItems = (
|
||||
affectedQueries: IAffectedQueryMap | null,
|
||||
selectedItem: string,
|
||||
): IDueToOperation[] | undefined => {
|
||||
if (affectedQueries) {
|
||||
return affectedQueries[selectedItem]?.dueToOperations;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getListOfItems = (affectedQueries: IAffectedQueryMap | null) => {
|
||||
const items = [];
|
||||
for (const key in affectedQueries) {
|
||||
if (affectedQueries.hasOwnProperty(key)) {
|
||||
items.push({ name: key, value: key });
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root: {
|
||||
minHeight: 0,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
TableColumnDefinition,
|
||||
createTableColumn,
|
||||
TableCellLayout,
|
||||
useScrollbarWidth,
|
||||
useFluent,
|
||||
Text,
|
||||
} from "@fluentui/react-components";
|
||||
import { IDueToOperation } from "apollo-inspector";
|
||||
import {
|
||||
DataGridBody,
|
||||
DataGrid,
|
||||
DataGridRow,
|
||||
DataGridHeader,
|
||||
DataGridCell,
|
||||
DataGridHeaderCell,
|
||||
} from "@fluentui/react-data-grid-react-window";
|
||||
import { useStyles } from "./affected-queries-grid-renderer-styles";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
const ItemSize = 40;
|
||||
export interface IAffectedQueriesGridRenderers {
|
||||
items: IDueToOperation[] | undefined;
|
||||
}
|
||||
|
||||
export const AffectedQueriesGridRenderers = (
|
||||
props: IAffectedQueriesGridRenderers,
|
||||
) => {
|
||||
const { items } = props;
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
const { targetDocument } = useFluent();
|
||||
const scrollbarWidth = useScrollbarWidth({ targetDocument });
|
||||
const divRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [gridHeight, setGridHeight] = React.useState(400);
|
||||
const classes = useStyles();
|
||||
|
||||
React.useEffect(() => {
|
||||
const height = divRef.current?.getBoundingClientRect().height;
|
||||
setGridHeight(height ? height - ItemSize : 400);
|
||||
const resizeObserver = new ResizeObserver(
|
||||
debounce(() => {
|
||||
const height = divRef.current?.getBoundingClientRect().height;
|
||||
const calcualtedHeight = height ? height - ItemSize : 400;
|
||||
setGridHeight(calcualtedHeight);
|
||||
}, 300),
|
||||
);
|
||||
resizeObserver.observe(document.body);
|
||||
return () => {
|
||||
resizeObserver.unobserve(document.body);
|
||||
};
|
||||
}, [divRef.current, setGridHeight]);
|
||||
|
||||
return (
|
||||
<div ref={divRef} className={classes.root}>
|
||||
<DataGrid
|
||||
items={items}
|
||||
columns={columns}
|
||||
focusMode="cell"
|
||||
resizableColumns
|
||||
columnSizingOptions={{
|
||||
operationName: { minWidth: 330, defaultWidth: 350 },
|
||||
id: { minWidth: 20, defaultWidth: 20 },
|
||||
}}
|
||||
>
|
||||
<DataGridHeader style={{ paddingRight: scrollbarWidth }}>
|
||||
<DataGridRow>
|
||||
{({ renderHeaderCell }) => (
|
||||
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
|
||||
)}
|
||||
</DataGridRow>
|
||||
</DataGridHeader>
|
||||
<DataGridBody<IDueToOperation> itemSize={40} height={gridHeight}>
|
||||
{({ item, rowId }, style) => (
|
||||
<DataGridRow<IDueToOperation> key={rowId} style={style}>
|
||||
{({ renderCell }) => (
|
||||
<DataGridCell>{renderCell(item)}</DataGridCell>
|
||||
)}
|
||||
</DataGridRow>
|
||||
)}
|
||||
</DataGridBody>
|
||||
</DataGrid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: TableColumnDefinition<IDueToOperation>[] = [
|
||||
createTableColumn<IDueToOperation>({
|
||||
columnId: "id",
|
||||
compare: (a, b) => {
|
||||
return a.id - b.id;
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return <Text weight="bold">{"Id"}</Text>;
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout>{item.id}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn({
|
||||
columnId: "Type",
|
||||
compare: (a, b) => {
|
||||
return a.operationType.localeCompare(b.operationType);
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return <Text weight="bold">{"Type"}</Text>;
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout>{item.operationType}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn({
|
||||
columnId: "operationName",
|
||||
compare: (a, b) => {
|
||||
if (a.operationName) {
|
||||
return a.operationName?.localeCompare(b.operationName || "");
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return <Text weight="bold">{"Operation Name"}</Text>;
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.operationName}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
];
|
|
@ -0,0 +1,19 @@
|
|||
import { makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
minHeight: 0,
|
||||
},
|
||||
rightPane: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
marginRight: "2rem",
|
||||
},
|
||||
rightPaneHeader: {
|
||||
display: "flex",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
TabList,
|
||||
Tab,
|
||||
SelectTabEvent,
|
||||
SelectTabData,
|
||||
Text,
|
||||
} from "@fluentui/react-components";
|
||||
import { AffectedQueriesGridRenderers } from "./affected-queries-grid-renderer";
|
||||
import { IDueToOperation } from "apollo-inspector";
|
||||
import { useStyles } from "./affected-queries-renderer-styles";
|
||||
|
||||
export interface IAffectedQueriesRendererProps {
|
||||
listOfItems: any[];
|
||||
gridItems: IDueToOperation[] | undefined;
|
||||
selectedListItem: string;
|
||||
onTabSelect: (event: SelectTabEvent, data: SelectTabData) => void;
|
||||
}
|
||||
|
||||
export const AffectedQueriesRenderer = (
|
||||
props: IAffectedQueriesRendererProps,
|
||||
) => {
|
||||
const { listOfItems, gridItems, selectedListItem, onTabSelect } = props;
|
||||
const classes = useStyles();
|
||||
const tabItems = listOfItems.map((element: any) => {
|
||||
return (
|
||||
<Tab key={element.name} value={element.name}>
|
||||
{element.value}
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<TabList
|
||||
selectedValue={selectedListItem}
|
||||
onTabSelect={onTabSelect}
|
||||
vertical={true}
|
||||
>
|
||||
{tabItems}
|
||||
</TabList>
|
||||
<div className={classes.rightPane}>
|
||||
<div className={classes.rightPaneHeader}>
|
||||
<Text weight="bold" size={300}>{`${selectedListItem}`}</Text>
|
||||
<span> </span>
|
||||
<Text
|
||||
size={300}
|
||||
>{`is re-rendered due to following operations in the table`}</Text>
|
||||
</div>
|
||||
<AffectedQueriesGridRenderers items={gridItems} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./affected-queries-container";
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import { Button, Title2 } from "@fluentui/react-components";
|
||||
|
||||
export interface IErrorBoundaryState {
|
||||
error?: IError;
|
||||
}
|
||||
export interface IError {
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
export class ErrorBoundary extends React.PureComponent<
|
||||
React.PropsWithChildren<unknown>,
|
||||
IErrorBoundaryState
|
||||
> {
|
||||
public state: IErrorBoundaryState = {};
|
||||
|
||||
public componentDidCatch(): void {
|
||||
this.setState({ error: { error: true, message: "Something went wrong" } });
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { children } = this.props;
|
||||
|
||||
if (this.state.error?.error) {
|
||||
return (
|
||||
<div>
|
||||
<Title2>{this.state.error.message}</Title2>
|
||||
<Button onClick={this.resetError}>Retry</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children as JSX.Element;
|
||||
}
|
||||
|
||||
private resetError() {
|
||||
this.setState({
|
||||
error: undefined,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
export interface IReducerState {
|
||||
export interface ICountReducerState {
|
||||
verboseOperationsCount: number;
|
||||
allOperationsCount: number;
|
||||
cacheOperationsCount: number;
|
||||
}
|
||||
|
||||
export enum ReducerActionEnum {
|
||||
export enum CountReducerActionEnum {
|
||||
UpdateVerboseOperationsCount,
|
||||
UpdateAllOperationsCount,
|
||||
UpdateCacheOperationsCount,
|
||||
|
||||
ClearVerboseOperations,
|
||||
}
|
||||
|
||||
export interface IReducerAction {
|
||||
type: ReducerActionEnum;
|
||||
export interface ICountReducerAction {
|
||||
type: CountReducerActionEnum;
|
||||
value: any;
|
||||
}
|
||||
|
|
|
@ -5,32 +5,39 @@ import { TabHeaders } from "../../../types";
|
|||
import { VerboseOperationsContainer } from "../verbose-operation/verbose-operations-container";
|
||||
import { useStyles } from "./operations-tracker-body-styles";
|
||||
import {
|
||||
IReducerState,
|
||||
IReducerAction,
|
||||
ReducerActionEnum,
|
||||
ICountReducerState,
|
||||
ICountReducerAction,
|
||||
CountReducerActionEnum,
|
||||
} from "./operations-tracker-body.interface";
|
||||
import { AffectedQueriesContainer } from "../affected-queries";
|
||||
import {
|
||||
IOperationsAction,
|
||||
IOperationsReducerState,
|
||||
} from "../operations-tracker-container-helper";
|
||||
|
||||
export interface IOperationViewRendererProps {
|
||||
selectedTab: TabHeaders;
|
||||
data: IDataView;
|
||||
filter: string;
|
||||
dispatchOperationsCount: React.Dispatch<IReducerAction>;
|
||||
operationsState: IOperationsReducerState;
|
||||
dispatchOperationsCount: React.Dispatch<ICountReducerAction>;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsAction>;
|
||||
}
|
||||
|
||||
export interface IOperationViewContainer {
|
||||
data: IDataView | null;
|
||||
filter: string;
|
||||
operationsState: IOperationsReducerState;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsAction>;
|
||||
}
|
||||
|
||||
const tabHeaders = [
|
||||
{ key: TabHeaders.AllOperationsView, name: "All operations" },
|
||||
{ key: TabHeaders.OperationsView, name: "Only Cache operations" },
|
||||
{ key: TabHeaders.VerboseOperationView, name: "Verbose operations" },
|
||||
{ key: TabHeaders.VerboseOperationView, name: "Operations" },
|
||||
{ key: TabHeaders.AffectedQueriesView, name: "Affected Queries" },
|
||||
];
|
||||
|
||||
export const OperationsTrackerBody = (props: IOperationViewContainer) => {
|
||||
const { data, filter } = props;
|
||||
const { data, operationsState, dispatchOperationsState } = props;
|
||||
const [selectedTab, setSelectedTab] = React.useState(
|
||||
TabHeaders.VerboseOperationView,
|
||||
);
|
||||
|
@ -45,12 +52,13 @@ export const OperationsTrackerBody = (props: IOperationViewContainer) => {
|
|||
const classes = useStyles();
|
||||
const updatedTabItems = React.useMemo(() => {
|
||||
const newTabHeaders = tabHeaders.filter(
|
||||
(tabHeader) => tabHeader.key === TabHeaders.VerboseOperationView,
|
||||
(tabHeader) =>
|
||||
tabHeader.key === TabHeaders.VerboseOperationView ||
|
||||
tabHeader.key === TabHeaders.AffectedQueriesView,
|
||||
);
|
||||
|
||||
return newTabHeaders;
|
||||
}, []);
|
||||
|
||||
const tabs = React.useMemo(() => {
|
||||
const items = updatedTabItems.map((item) => {
|
||||
return (
|
||||
|
@ -85,23 +93,39 @@ export const OperationsTrackerBody = (props: IOperationViewContainer) => {
|
|||
<OperationsViewRenderer
|
||||
data={data}
|
||||
selectedTab={selectedTab}
|
||||
filter={filter}
|
||||
operationsState={operationsState}
|
||||
dispatchOperationsCount={dispatchOperationsCount}
|
||||
dispatchOperationsState={dispatchOperationsState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OperationsViewRenderer = (props: IOperationViewRendererProps) => {
|
||||
const { selectedTab, data, filter, dispatchOperationsCount } = props;
|
||||
const {
|
||||
selectedTab,
|
||||
data,
|
||||
operationsState,
|
||||
dispatchOperationsCount,
|
||||
dispatchOperationsState,
|
||||
} = props;
|
||||
|
||||
switch (selectedTab) {
|
||||
case TabHeaders.VerboseOperationView: {
|
||||
return (
|
||||
<VerboseOperationsContainer
|
||||
operations={data.verboseOperations}
|
||||
filter={filter}
|
||||
operationsState={operationsState}
|
||||
dispatchOperationsCount={dispatchOperationsCount}
|
||||
dispatchOperationsState={dispatchOperationsState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case TabHeaders.AffectedQueriesView: {
|
||||
return (
|
||||
<AffectedQueriesContainer
|
||||
affectedQueries={data.affectedQueriesOperations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -112,7 +136,7 @@ const OperationsViewRenderer = (props: IOperationViewRendererProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getCount = (key: TabHeaders, data: IReducerState) => {
|
||||
const getCount = (key: TabHeaders, data: ICountReducerState) => {
|
||||
switch (key) {
|
||||
case TabHeaders.VerboseOperationView: {
|
||||
return data.verboseOperationsCount;
|
||||
|
@ -131,17 +155,17 @@ const getCount = (key: TabHeaders, data: IReducerState) => {
|
|||
};
|
||||
|
||||
const reducer = (
|
||||
state: IReducerState,
|
||||
action: IReducerAction,
|
||||
): IReducerState => {
|
||||
state: ICountReducerState,
|
||||
action: ICountReducerAction,
|
||||
): ICountReducerState => {
|
||||
switch (action.type) {
|
||||
case ReducerActionEnum.UpdateAllOperationsCount: {
|
||||
case CountReducerActionEnum.UpdateAllOperationsCount: {
|
||||
return { ...state, allOperationsCount: action.value };
|
||||
}
|
||||
case ReducerActionEnum.UpdateCacheOperationsCount: {
|
||||
case CountReducerActionEnum.UpdateCacheOperationsCount: {
|
||||
return { ...state, cacheOperationsCount: action.value };
|
||||
}
|
||||
case ReducerActionEnum.UpdateVerboseOperationsCount: {
|
||||
case CountReducerActionEnum.UpdateVerboseOperationsCount: {
|
||||
return { ...state, verboseOperationsCount: action.value };
|
||||
}
|
||||
|
||||
|
@ -151,7 +175,9 @@ const reducer = (
|
|||
}
|
||||
};
|
||||
|
||||
const computeInitialReducerState = (data: IDataView | null): IReducerState => {
|
||||
const computeInitialReducerState = (
|
||||
data: IDataView | null,
|
||||
): ICountReducerState => {
|
||||
return {
|
||||
allOperationsCount: data?.allOperations?.length || 0,
|
||||
verboseOperationsCount: data?.verboseOperations?.length || 0,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { IVerboseOperation } from "apollo-inspector";
|
||||
|
||||
export interface IOperationsReducerState {
|
||||
searchText: string;
|
||||
checkedOperations: IVerboseOperation[] | null;
|
||||
filteredOperations: IVerboseOperation[] | null;
|
||||
selectedOperation: IVerboseOperation | null | undefined;
|
||||
}
|
||||
|
||||
export interface IOperationsAction {
|
||||
type: OperationReducerActionEnum;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export enum OperationReducerActionEnum {
|
||||
UpdateSearchText,
|
||||
UpdateCheckedOperations,
|
||||
UpdateFilteredOperations,
|
||||
UpdateSelectedOperation,
|
||||
}
|
||||
|
||||
export const reducers = (
|
||||
state: IOperationsReducerState,
|
||||
action: IOperationsAction,
|
||||
): IOperationsReducerState => {
|
||||
switch (action.type) {
|
||||
case OperationReducerActionEnum.UpdateSearchText: {
|
||||
return { ...state, searchText: action.value };
|
||||
}
|
||||
|
||||
case OperationReducerActionEnum.UpdateCheckedOperations: {
|
||||
return { ...state, checkedOperations: action.value };
|
||||
}
|
||||
|
||||
case OperationReducerActionEnum.UpdateFilteredOperations: {
|
||||
return { ...state, filteredOperations: action.value };
|
||||
}
|
||||
|
||||
case OperationReducerActionEnum.UpdateSelectedOperation: {
|
||||
return { ...state, selectedOperation: action.value };
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getInitialState = (): IOperationsReducerState => {
|
||||
return {
|
||||
checkedOperations: null,
|
||||
filteredOperations: null,
|
||||
searchText: "",
|
||||
selectedOperation: null,
|
||||
};
|
||||
};
|
|
@ -7,6 +7,8 @@ export const useStyles = makeStyles({
|
|||
flexBasic: 0,
|
||||
...shorthands.padding("10px"),
|
||||
display: "flex",
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
},
|
||||
innerContainer: {
|
||||
display: "flex",
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { IDataView } from "apollo-inspector";
|
||||
import {
|
||||
IOperationsAction,
|
||||
IOperationsReducerState,
|
||||
} from "./operations-tracker-container-helper";
|
||||
|
||||
export interface IError {
|
||||
error: any;
|
||||
|
@ -22,7 +26,8 @@ export interface IUseMainSlotParams {
|
|||
error: IError | null;
|
||||
loader: ILoader;
|
||||
apollOperationsData: IDataView | null;
|
||||
filter: string;
|
||||
operationsState: IOperationsReducerState;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsAction>;
|
||||
}
|
||||
|
||||
export interface IUseMainSlotService {
|
||||
|
|
|
@ -11,6 +11,12 @@ import {
|
|||
IUseMainSlotParams,
|
||||
IUseMainSlotService,
|
||||
} from "./operations-tracker-container.interface";
|
||||
import { ErrorBoundary } from "./operation-tracker-error-boundary";
|
||||
import {
|
||||
getInitialState,
|
||||
OperationReducerActionEnum,
|
||||
reducers,
|
||||
} from "./operations-tracker-container-helper";
|
||||
|
||||
export const OperationsTrackerContainer = () => {
|
||||
const [openDescription, setOpenDescription] = useState<boolean>(false);
|
||||
|
@ -24,7 +30,12 @@ export const OperationsTrackerContainer = () => {
|
|||
});
|
||||
const [error, setError] = React.useState<IError | null>(null);
|
||||
const [isRecording, setIsRecording] = useState<boolean>(false);
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
|
||||
const [operationsState, dispatchOperationsState] = React.useReducer(
|
||||
reducers,
|
||||
getInitialState(),
|
||||
);
|
||||
|
||||
const classes = useStyles();
|
||||
useSubscribeToPublisher(setError, setApolloOperationsData, setLoader);
|
||||
|
||||
|
@ -34,6 +45,13 @@ export const OperationsTrackerContainer = () => {
|
|||
setLoader,
|
||||
setError,
|
||||
);
|
||||
React.useMemo(() => {
|
||||
return null;
|
||||
}, [operationsState]);
|
||||
|
||||
const clearApolloOperations = useCallback(() => {
|
||||
setApolloOperationsData(null);
|
||||
}, [setApolloOperationsData]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -42,53 +60,86 @@ export const OperationsTrackerContainer = () => {
|
|||
}, [remplSubscriber]);
|
||||
|
||||
const mainSlot = useMainSlot(
|
||||
{ error, loader, apollOperationsData, filter },
|
||||
{
|
||||
error,
|
||||
loader,
|
||||
apollOperationsData,
|
||||
operationsState,
|
||||
dispatchOperationsState,
|
||||
},
|
||||
{ classes },
|
||||
);
|
||||
|
||||
const setSearchText = React.useCallback(
|
||||
(text) => {
|
||||
dispatchOperationsState({
|
||||
type: OperationReducerActionEnum.UpdateSearchText,
|
||||
value: text,
|
||||
});
|
||||
},
|
||||
[dispatchOperationsState],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div
|
||||
className={mergeClasses(
|
||||
classes.innerContainer,
|
||||
openDescription && classes.innerContainerDescription,
|
||||
)}
|
||||
>
|
||||
<OperationsTrackerHeader
|
||||
isRecording={isRecording}
|
||||
openDescription={openDescription}
|
||||
setOpenDescription={setOpenDescription}
|
||||
toggleRecording={toggleRecording}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
{mainSlot}
|
||||
<ErrorBoundary>
|
||||
<div className={classes.root}>
|
||||
<div
|
||||
className={mergeClasses(
|
||||
classes.innerContainer,
|
||||
openDescription && classes.innerContainerDescription,
|
||||
)}
|
||||
>
|
||||
<OperationsTrackerHeader
|
||||
isRecording={isRecording}
|
||||
openDescription={openDescription}
|
||||
setOpenDescription={setOpenDescription}
|
||||
toggleRecording={toggleRecording}
|
||||
setSearchText={setSearchText}
|
||||
operationsState={operationsState}
|
||||
apollOperationsData={apollOperationsData}
|
||||
clearApolloOperations={clearApolloOperations}
|
||||
showClear={!!apollOperationsData?.verboseOperations}
|
||||
/>
|
||||
{mainSlot}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const useMainSlot = (
|
||||
{ apollOperationsData, error, loader, filter }: IUseMainSlotParams,
|
||||
{
|
||||
apollOperationsData,
|
||||
error,
|
||||
loader,
|
||||
dispatchOperationsState,
|
||||
operationsState,
|
||||
}: IUseMainSlotParams,
|
||||
{ classes }: IUseMainSlotService,
|
||||
) =>
|
||||
React.useMemo(() => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className={classes.centerDiv}>
|
||||
<Title2>{error.message}</Title2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loader.loading) {
|
||||
return (
|
||||
<div className={classes.centerDiv}>
|
||||
<Spinner labelPosition="below" label={loader.message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className={classes.centerDiv}>
|
||||
<Title2>{error.message}</Title2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loader.loading) {
|
||||
return (
|
||||
<div className={classes.centerDiv}>
|
||||
<Spinner labelPosition="below" label={loader.message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OperationsTrackerBody data={apollOperationsData} filter={filter} />;
|
||||
}, [error, loader, apollOperationsData, classes, filter]);
|
||||
return (
|
||||
<OperationsTrackerBody
|
||||
dispatchOperationsState={dispatchOperationsState}
|
||||
data={apollOperationsData}
|
||||
operationsState={operationsState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useToggleRecording = (
|
||||
setIsRecording: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
button: {
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
MenuPopover,
|
||||
MenuTrigger,
|
||||
SplitButton,
|
||||
MenuButtonProps,
|
||||
Button,
|
||||
} from "@fluentui/react-components";
|
||||
import { remplSubscriber } from "../../rempl";
|
||||
import * as React from "react";
|
||||
import { IOperationsReducerState } from "../operations-tracker-container-helper";
|
||||
import { useStyles } from "./operations-copy-button-styles";
|
||||
import { IDataView } from "apollo-inspector";
|
||||
|
||||
export interface ICopyButtonProps {
|
||||
hideCopy: boolean;
|
||||
operationsState: IOperationsReducerState;
|
||||
apolloOperationsData: IDataView | null;
|
||||
}
|
||||
|
||||
export const CopyButton = (props: ICopyButtonProps) => {
|
||||
const classes = useStyles();
|
||||
const { operationsState, hideCopy, apolloOperationsData } = props;
|
||||
|
||||
const copyAll = React.useCallback(() => {
|
||||
const ids: number[] = [];
|
||||
apolloOperationsData?.verboseOperations?.forEach((op) => {
|
||||
ids.push(op.id);
|
||||
});
|
||||
remplSubscriber.callRemote("copyOperationsData", ids);
|
||||
}, [apolloOperationsData]);
|
||||
|
||||
const copyFiltered = React.useCallback(() => {
|
||||
const ids: number[] = [];
|
||||
operationsState.filteredOperations?.forEach((op) => {
|
||||
ids.push(op.id);
|
||||
});
|
||||
|
||||
remplSubscriber.callRemote("copyOperationsData", ids);
|
||||
}, [operationsState]);
|
||||
|
||||
const copyChecked = React.useCallback(() => {
|
||||
const ids: number[] = [];
|
||||
operationsState.checkedOperations?.forEach((op) => {
|
||||
ids.push(op.id);
|
||||
});
|
||||
|
||||
remplSubscriber.callRemote("copyOperationsData", ids);
|
||||
}, [operationsState]);
|
||||
|
||||
const copySelected = React.useCallback(() => {
|
||||
if (operationsState.selectedOperation?.id) {
|
||||
const ids: number[] = [operationsState.selectedOperation.id];
|
||||
|
||||
remplSubscriber.callRemote("copyOperationsData", ids);
|
||||
}
|
||||
}, [operationsState]);
|
||||
|
||||
const copyCache = React.useCallback(() => {
|
||||
remplSubscriber.callRemote("copyOperationsData", [-1]);
|
||||
}, [operationsState]);
|
||||
|
||||
if (hideCopy) {
|
||||
return (
|
||||
<div className={classes.button}>
|
||||
<Button onClick={copyCache}>Copy Whole Apollo Cache</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.button}>
|
||||
<Menu positioning="below-end">
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
{(triggerProps: MenuButtonProps) => (
|
||||
<SplitButton
|
||||
disabled={hideCopy}
|
||||
onClick={copyAll}
|
||||
menuButton={triggerProps}
|
||||
>
|
||||
Copy All
|
||||
</SplitButton>
|
||||
)}
|
||||
</MenuTrigger>
|
||||
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
<MenuItem onClick={copyAll}>Copy All Operations</MenuItem>
|
||||
{(operationsState.filteredOperations?.length || 0) > 0 ? (
|
||||
<MenuItem onClick={copyFiltered}>
|
||||
Copy Filtered Operations
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{(operationsState.checkedOperations?.length || 0) > 0 ? (
|
||||
<MenuItem onClick={copyChecked}>Copy Checked Operations</MenuItem>
|
||||
) : null}
|
||||
{operationsState.selectedOperation ? (
|
||||
<MenuItem onClick={copySelected}>
|
||||
Copy currently Opened Operation
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem onClick={copyCache}>Copy Whole Apollo Cache</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -23,4 +23,7 @@ export const useStyles = makeStyles({
|
|||
height: "auto",
|
||||
...shorthands.overflow("hidden", "auto"),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,60 +4,86 @@ import { Info20Regular } from "@fluentui/react-icons";
|
|||
import { useStyles } from "./operations-tracker-header-styles";
|
||||
import { Search } from "../../../components";
|
||||
import debounce from "lodash.debounce";
|
||||
import { CopyButton } from "./operations-copy-button";
|
||||
import { IOperationsReducerState } from "../operations-tracker-container-helper";
|
||||
import { IDataView } from "apollo-inspector";
|
||||
|
||||
export interface IOperationsTrackerHeaderProps {
|
||||
setOpenDescription: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
openDescription: boolean;
|
||||
toggleRecording: () => void;
|
||||
isRecording: boolean;
|
||||
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSearchText: React.Dispatch<React.SetStateAction<string>>;
|
||||
operationsState: IOperationsReducerState;
|
||||
apollOperationsData: IDataView | null;
|
||||
clearApolloOperations: () => void;
|
||||
showClear: boolean;
|
||||
}
|
||||
|
||||
export const OperationsTrackerHeader = (
|
||||
props: IOperationsTrackerHeaderProps,
|
||||
) => {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
isRecording,
|
||||
openDescription,
|
||||
setOpenDescription,
|
||||
toggleRecording,
|
||||
setFilter,
|
||||
} = props;
|
||||
export const OperationsTrackerHeader = React.memo(
|
||||
(props: IOperationsTrackerHeaderProps) => {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
isRecording,
|
||||
openDescription,
|
||||
setOpenDescription,
|
||||
toggleRecording,
|
||||
setSearchText,
|
||||
operationsState,
|
||||
clearApolloOperations,
|
||||
showClear,
|
||||
apollOperationsData,
|
||||
} = props;
|
||||
|
||||
const debouncedFilter = React.useCallback(
|
||||
debounce((e: React.SyntheticEvent) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
setFilter(input.value);
|
||||
}, 200),
|
||||
[setFilter],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<div>
|
||||
<Button
|
||||
title="Information"
|
||||
tabIndex={0}
|
||||
className={classes.infoButton}
|
||||
onClick={() => setOpenDescription(!openDescription)}
|
||||
>
|
||||
<Info20Regular />
|
||||
</Button>
|
||||
<Button onClick={toggleRecording}>
|
||||
{isRecording ? "Stop recording" : "Record recent activity"}
|
||||
</Button>
|
||||
const debouncedFilter = React.useCallback(
|
||||
debounce((e: React.SyntheticEvent) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
setSearchText(input.value);
|
||||
}, 200),
|
||||
[setSearchText],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
title="Information"
|
||||
tabIndex={0}
|
||||
className={classes.infoButton}
|
||||
onClick={() => setOpenDescription(!openDescription)}
|
||||
>
|
||||
<Info20Regular />
|
||||
</Button>
|
||||
<Button onClick={toggleRecording}>
|
||||
{isRecording ? "Stop" : "Record"}
|
||||
</Button>
|
||||
<CopyButton
|
||||
hideCopy={isRecording || !showClear}
|
||||
operationsState={operationsState}
|
||||
apolloOperationsData={apollOperationsData}
|
||||
/>
|
||||
{isRecording || !showClear ? null : (
|
||||
<Button
|
||||
style={{ marginLeft: "0.5rem" }}
|
||||
onClick={clearApolloOperations}
|
||||
disabled={!showClear}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Search onSearchChange={debouncedFilter} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Search onSearchChange={debouncedFilter} />
|
||||
</div>
|
||||
</div>
|
||||
{openDescription && (
|
||||
<div className={classes.description}>
|
||||
It monitors changes in cache, fired mutations and
|
||||
activated/deactivated queries.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
{openDescription && (
|
||||
<div className={classes.description}>
|
||||
It monitors changes in cache, fired mutations and
|
||||
activated/deactivated queries.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { DocumentNode, OperationDefinitionNode } from "graphql";
|
||||
import { DocumentNode, getOperationAST } from "graphql";
|
||||
|
||||
export const getOperationName = (query: DocumentNode) => {
|
||||
const definition =
|
||||
query && query.definitions && query.definitions.length > 0
|
||||
? (query.definitions[0] as OperationDefinitionNode)
|
||||
: null;
|
||||
const definition = getOperationAST(query);
|
||||
const operationName = definition ? definition.name?.value : "name_not_found";
|
||||
|
||||
return operationName;
|
||||
|
@ -20,9 +17,49 @@ export const isNumber = (input: string | number | undefined = "NA") => {
|
|||
};
|
||||
|
||||
export const copyToClipboard = async (obj: unknown) => {
|
||||
try {
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
console.log(`failed to copy`, error);
|
||||
}
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
export const secondsToTime = (time: number) => {
|
||||
const seconds = parseFloat((time / 1000).toFixed(4));
|
||||
let min = -1;
|
||||
let hour = -1;
|
||||
let format = "";
|
||||
if (seconds > 60) {
|
||||
min = parseFloat((seconds / 60).toFixed(4));
|
||||
}
|
||||
if (min > 60) {
|
||||
hour = parseFloat((min / 60).toFixed(4));
|
||||
}
|
||||
|
||||
if (hour >= 1) {
|
||||
format += `${hour} hour `;
|
||||
}
|
||||
|
||||
if (min >= 1) {
|
||||
format += `${min} min `;
|
||||
}
|
||||
|
||||
format += `${seconds} sec`;
|
||||
|
||||
return format.trim();
|
||||
};
|
||||
|
||||
export const sizeInBytes = (size: number) => {
|
||||
if (!size) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const kb = parseFloat((size / 1024).toFixed(4));
|
||||
const mb = parseFloat((kb / 1024).toFixed(4));
|
||||
const gb = parseFloat((mb / 1024).toFixed(4));
|
||||
|
||||
if (gb >= 1) {
|
||||
return `${gb}GB`;
|
||||
}
|
||||
|
||||
if (mb >= 1) {
|
||||
return `${mb}MB`;
|
||||
}
|
||||
return `${kb}KB`;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
TableCellLayout,
|
||||
createTableColumn,
|
||||
Text,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
IOperationResult,
|
||||
IVerboseOperation,
|
||||
OperationType,
|
||||
} from "apollo-inspector";
|
||||
import { fragmentSubTypes, IFilterSet, querySubTypes } from "./filter-view";
|
||||
import {
|
||||
secondsToTime,
|
||||
sizeInBytes,
|
||||
} from "../utils/apollo-operations-tracker-utils";
|
||||
import {
|
||||
EditRegular,
|
||||
ReadingListRegular,
|
||||
PipelineRegular,
|
||||
DatabaseLinkRegular,
|
||||
BookDatabaseRegular,
|
||||
DatabaseSearchRegular,
|
||||
DatabaseRegular,
|
||||
BookOpenRegular,
|
||||
BookLetterRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
|
||||
export type Duration = {
|
||||
totalTime: number;
|
||||
};
|
||||
|
||||
export type Timing = {
|
||||
queuedAt: number;
|
||||
dataWrittenToCacheCompletedAt: number;
|
||||
responseReceivedFromServerAt: number;
|
||||
};
|
||||
|
||||
export type Result = {
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type Item = {
|
||||
operationType: string;
|
||||
operationName: string;
|
||||
isActive: boolean;
|
||||
duration: Duration;
|
||||
timing: Timing;
|
||||
status: string;
|
||||
fetchPolicy: string;
|
||||
result: Result[];
|
||||
id: number;
|
||||
};
|
||||
|
||||
export const getColumns = (
|
||||
anyOperationSelected: boolean,
|
||||
classes: Record<
|
||||
| "gridRow"
|
||||
| "gridBody"
|
||||
| "gridHeader"
|
||||
| "gridView"
|
||||
| "selectedAndFailedRow"
|
||||
| "failedRow"
|
||||
| "selectedRow"
|
||||
| "operationText",
|
||||
string
|
||||
>,
|
||||
) => {
|
||||
if (anyOperationSelected) {
|
||||
return [
|
||||
createTableColumn<Item>({
|
||||
columnId: "id",
|
||||
renderHeaderCell: () => {
|
||||
return "Id";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return b.id - a.id;
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.id}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "operationType",
|
||||
renderHeaderCell: () => {
|
||||
return "Type";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return compareString(b.operationType, a.operationType);
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return (
|
||||
<TableCellLayout
|
||||
truncate
|
||||
media={getOperationIcon(item.operationType)}
|
||||
>
|
||||
{item.operationType}
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "operationName",
|
||||
renderHeaderCell: () => {
|
||||
return "Name";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return compareString(b.operationName, a.operationName);
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return (
|
||||
<TableCellLayout truncate>
|
||||
<Text truncate wrap={false} className={classes.operationText}>
|
||||
{item.operationName}
|
||||
</Text>
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [
|
||||
createTableColumn<Item>({
|
||||
columnId: "id",
|
||||
renderHeaderCell: () => {
|
||||
return "Id";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return b.id - a.id;
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.id}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "operationType",
|
||||
renderHeaderCell: () => {
|
||||
return "Type";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return compareString(b.operationType, a.operationType);
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return (
|
||||
<TableCellLayout
|
||||
truncate
|
||||
media={getOperationIcon(item.operationType)}
|
||||
>
|
||||
{item.operationType}
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "operationName",
|
||||
renderHeaderCell: () => {
|
||||
return "Name";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return compareString(b.operationName, a.operationName);
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.operationName}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "status",
|
||||
compare: (a, b) => {
|
||||
return compareString(b.status, a.status);
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return "Status";
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.status}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "fetchPolicy",
|
||||
compare: (a, b) => {
|
||||
return compareString(b.fetchPolicy, a.fetchPolicy);
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return "Fetch Policy";
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return <TableCellLayout truncate>{item.fetchPolicy}</TableCellLayout>;
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "totalTime",
|
||||
compare: (a, b) => {
|
||||
return b.duration.totalTime - a.duration.totalTime;
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return "Total Exec time";
|
||||
},
|
||||
renderCell: (item) => {
|
||||
if (isNaN(item.duration.totalTime)) {
|
||||
return <TableCellLayout truncate>{``}</TableCellLayout>;
|
||||
}
|
||||
return (
|
||||
<TableCellLayout truncate>
|
||||
{item.duration.totalTime > 1000
|
||||
? secondsToTime(item.duration.totalTime)
|
||||
: `${item.duration.totalTime} ms`}
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "queuedAt",
|
||||
compare: (a, b) => {
|
||||
return b.timing.queuedAt - a.timing.queuedAt;
|
||||
},
|
||||
renderHeaderCell: () => {
|
||||
return "Queued at";
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return (
|
||||
<TableCellLayout truncate>
|
||||
{item.timing.queuedAt > 1000
|
||||
? secondsToTime(item.timing.queuedAt)
|
||||
: `${item.timing.queuedAt} ms`}
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
createTableColumn<Item>({
|
||||
columnId: "size",
|
||||
renderHeaderCell: () => {
|
||||
return "Size";
|
||||
},
|
||||
compare: (a, b) => {
|
||||
return (b.result[0]?.size || 0) - (a.result[0]?.size || 0);
|
||||
},
|
||||
renderCell: (item) => {
|
||||
return (
|
||||
<TableCellLayout truncate>
|
||||
{sizeInBytes(item.result[0]?.size)}
|
||||
</TableCellLayout>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export const getFilteredItems = (
|
||||
items: IVerboseOperation[] | null | undefined,
|
||||
searchText: string,
|
||||
filters: IFilterSet | null,
|
||||
) => {
|
||||
let filteredItems = items || [];
|
||||
if (searchText.length > 0) {
|
||||
const tokens = searchText
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x !== "");
|
||||
filteredItems =
|
||||
tokens.length > 0
|
||||
? filteredItems.filter((item) => {
|
||||
return tokens.find((x) =>
|
||||
item.operationName?.toLowerCase().includes(x.toLowerCase()),
|
||||
);
|
||||
})
|
||||
: filteredItems;
|
||||
}
|
||||
if (filters) {
|
||||
// filtering based on types
|
||||
let filterTypes = filters.types.concat([]).map((x) => x.toLowerCase());
|
||||
if (filterTypes.includes(OperationType.Query.toLowerCase())) {
|
||||
filterTypes = filterTypes.concat(querySubTypes);
|
||||
}
|
||||
if (filterTypes.includes(OperationType.Fragment.toLowerCase())) {
|
||||
filterTypes = filterTypes.concat(fragmentSubTypes);
|
||||
}
|
||||
if (filterTypes.length > 0) {
|
||||
filteredItems = filteredItems.filter((item) =>
|
||||
filterTypes.includes(item.operationType.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
// filtering based on results
|
||||
const results = filters.results.concat([]).map((x) => x.toLowerCase());
|
||||
if (results.length > 0) {
|
||||
filteredItems = filteredItems.filter((item) => {
|
||||
const fromResult = (item.result?.[0] as IOperationResult)?.from;
|
||||
return results.includes((fromResult || "").toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// filtering based on status
|
||||
const statuses = filters.statuses.concat([]).map((x) => x.toLowerCase());
|
||||
if (statuses.length > 0) {
|
||||
filteredItems = filteredItems.filter((item) =>
|
||||
statuses.includes(item.status.toLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
const getOperationIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case OperationType.Query: {
|
||||
return <ReadingListRegular />;
|
||||
}
|
||||
case OperationType.Mutation: {
|
||||
return <EditRegular />;
|
||||
}
|
||||
case OperationType.Subscription: {
|
||||
return <PipelineRegular />;
|
||||
}
|
||||
case OperationType.CacheReadQuery: {
|
||||
return <DatabaseSearchRegular />;
|
||||
}
|
||||
case OperationType.CacheWriteQuery: {
|
||||
return <DatabaseLinkRegular />;
|
||||
}
|
||||
case OperationType.CacheReadFragment: {
|
||||
return <BookDatabaseRegular />;
|
||||
}
|
||||
case OperationType.CacheWriteFragment: {
|
||||
return <DatabaseRegular />;
|
||||
}
|
||||
case OperationType.ClientReadFragment: {
|
||||
return <BookOpenRegular />;
|
||||
}
|
||||
case OperationType.ClientWriteFragment: {
|
||||
return <BookLetterRegular />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const compareString = (a: string | undefined, b: string | undefined) => {
|
||||
return (a || "").localeCompare(b || "");
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
import { makeStyles, shorthands } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
gridBody: {
|
||||
position: "relative",
|
||||
willChange: "transform",
|
||||
direction: "ltr",
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflowY: "scroll",
|
||||
"::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
gridRow: {
|
||||
":hover": {
|
||||
backgroundColor: "unset",
|
||||
color: "unset",
|
||||
},
|
||||
},
|
||||
gridHeader: {
|
||||
":hover": {
|
||||
backgroundColor: "unset !important",
|
||||
color: "unset !important",
|
||||
},
|
||||
},
|
||||
gridView: {
|
||||
minWidth: 0,
|
||||
height: "100%",
|
||||
flexGrow: 2,
|
||||
display: "flex",
|
||||
"&:hover": {
|
||||
backgroundColor: "unset !important",
|
||||
color: "unset !important",
|
||||
},
|
||||
},
|
||||
selectedAndFailedRow: {
|
||||
color: "darkred",
|
||||
backgroundColor: "darkgrey",
|
||||
fontWeight: "bold",
|
||||
"&:hover": {
|
||||
backgroundColor: "darkgrey",
|
||||
color: "darkred",
|
||||
},
|
||||
},
|
||||
failedRow: {
|
||||
"&:hover": {
|
||||
backgroundColor: "unset",
|
||||
color: "red",
|
||||
},
|
||||
color: "red",
|
||||
},
|
||||
selectedRow: {
|
||||
backgroundColor: "darkgrey",
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
"&:hover": {
|
||||
backgroundColor: "darkgrey",
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
operationText: {
|
||||
...shorthands.overflow("hidden"),
|
||||
display: "block",
|
||||
},
|
||||
selectedOperationGridWrapper: {
|
||||
minWidth: 0,
|
||||
flexGrow: 2,
|
||||
},
|
||||
gridWrapper: {
|
||||
flexGrow: 2,
|
||||
},
|
||||
filterViewWrapper: {
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,241 @@
|
|||
import * as React from "react";
|
||||
import { useScrollbarWidth, useFluent } from "@fluentui/react-components";
|
||||
import {
|
||||
DataGridBody,
|
||||
DataGrid,
|
||||
DataGridRow,
|
||||
DataGridHeader,
|
||||
DataGridCell,
|
||||
DataGridHeaderCell,
|
||||
} from "@fluentui/react-data-grid-react-window";
|
||||
import { IVerboseOperation } from "apollo-inspector";
|
||||
import { useStyles } from "./data-grid-view.styles";
|
||||
import {
|
||||
ICountReducerAction,
|
||||
CountReducerActionEnum,
|
||||
} from "../operations-tracker-body";
|
||||
import { FilterView, IFilterSet } from "./filter-view";
|
||||
import debounce from "lodash.debounce";
|
||||
import { getColumns, getFilteredItems, Item } from "./data-grid-view-helper";
|
||||
import {
|
||||
IOperationsAction,
|
||||
IOperationsReducerState,
|
||||
OperationReducerActionEnum,
|
||||
} from "../operations-tracker-container-helper";
|
||||
|
||||
export interface IDataGridView {
|
||||
operations: IVerboseOperation[] | null;
|
||||
operationsState: IOperationsReducerState;
|
||||
dispatchOperationsCount: React.Dispatch<ICountReducerAction>;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsAction>;
|
||||
}
|
||||
|
||||
const ItemSize = 40;
|
||||
|
||||
export const DataGridView = (props: IDataGridView) => {
|
||||
const {
|
||||
operations,
|
||||
operationsState,
|
||||
dispatchOperationsCount,
|
||||
dispatchOperationsState,
|
||||
} = props;
|
||||
const { targetDocument } = useFluent();
|
||||
const scrollbarWidth = useScrollbarWidth({ targetDocument });
|
||||
const [gridHeight, setGridHeight] = React.useState(400);
|
||||
const divRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const height = divRef.current?.getBoundingClientRect().height;
|
||||
setGridHeight(height ? height - ItemSize : 400);
|
||||
const resizeObserver = new ResizeObserver(
|
||||
debounce(() => {
|
||||
const height = divRef.current?.getBoundingClientRect().height;
|
||||
const calcualtedHeight = height ? height - ItemSize : 400;
|
||||
setGridHeight(calcualtedHeight);
|
||||
}, 300),
|
||||
);
|
||||
resizeObserver.observe(document.body);
|
||||
return () => {
|
||||
resizeObserver.unobserve(document.body);
|
||||
};
|
||||
}, [divRef.current, setGridHeight]);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const filteredOperations: IVerboseOperation[] =
|
||||
props.operations?.concat([]) ?? [];
|
||||
|
||||
const [filters, setFilters] = React.useState<IFilterSet | null>(null);
|
||||
const [filteredItems, setFilteredItems] = React.useState(operations || []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const items = getFilteredItems(
|
||||
operations,
|
||||
operationsState.searchText,
|
||||
filters,
|
||||
);
|
||||
setFilteredItems(items);
|
||||
dispatchOperationsCount({
|
||||
type: CountReducerActionEnum.UpdateVerboseOperationsCount,
|
||||
value: items?.length,
|
||||
});
|
||||
dispatchOperationsState({
|
||||
type: OperationReducerActionEnum.UpdateFilteredOperations,
|
||||
value: items,
|
||||
});
|
||||
}, [
|
||||
filters,
|
||||
operationsState.searchText,
|
||||
operations,
|
||||
dispatchOperationsCount,
|
||||
setFilteredItems,
|
||||
dispatchOperationsState,
|
||||
]);
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => getColumns(!!operationsState.selectedOperation, classes),
|
||||
[operationsState.selectedOperation, classes],
|
||||
);
|
||||
|
||||
const operationsMap = React.useMemo(() => {
|
||||
const map = new Map<number, IVerboseOperation>();
|
||||
filteredOperations?.forEach((op) => {
|
||||
map.set(op.id, op);
|
||||
});
|
||||
return map;
|
||||
}, [filteredOperations]);
|
||||
|
||||
const onClick = React.useCallback(
|
||||
(item) => {
|
||||
const operation = operationsMap.get(item.id);
|
||||
dispatchOperationsState({
|
||||
type: OperationReducerActionEnum.UpdateSelectedOperation,
|
||||
value: operation,
|
||||
});
|
||||
},
|
||||
[dispatchOperationsState, operationsMap],
|
||||
);
|
||||
|
||||
const updateFilters = React.useCallback(
|
||||
(input: IFilterSet | null) => {
|
||||
setFilters(input);
|
||||
},
|
||||
[setFilters],
|
||||
);
|
||||
|
||||
const updateVerboseOperations = React.useCallback(
|
||||
(e, { selectedItems }) => {
|
||||
setTimeout(() => {
|
||||
const operations: IVerboseOperation[] = [];
|
||||
[...selectedItems].forEach((index) =>
|
||||
operations.push(filteredItems[index]),
|
||||
);
|
||||
|
||||
dispatchOperationsState({
|
||||
type: OperationReducerActionEnum.UpdateCheckedOperations,
|
||||
value: operations,
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
[dispatchOperationsState, filteredItems],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.gridView} ref={divRef}>
|
||||
<div className={classes.filterViewWrapper}>
|
||||
<FilterView setFilters={updateFilters} />
|
||||
</div>
|
||||
<div
|
||||
{...(operationsState.selectedOperation
|
||||
? { className: classes.selectedOperationGridWrapper }
|
||||
: { className: classes.filterViewWrapper })}
|
||||
>
|
||||
<DataGrid
|
||||
items={filteredItems as any}
|
||||
columns={columns}
|
||||
focusMode="cell"
|
||||
sortable
|
||||
resizableColumns
|
||||
columnSizingOptions={{
|
||||
id: {
|
||||
minWidth: 40,
|
||||
defaultWidth: 50,
|
||||
},
|
||||
status: {
|
||||
minWidth: 30,
|
||||
defaultWidth: 80,
|
||||
},
|
||||
fetchPolicy: {
|
||||
minWidth: 30,
|
||||
},
|
||||
totalTime: {
|
||||
minWidth: 30,
|
||||
defaultWidth: 70,
|
||||
},
|
||||
queuedAt: {
|
||||
minWidth: 30,
|
||||
defaultWidth: 90,
|
||||
},
|
||||
size: {
|
||||
minWidth: 30,
|
||||
},
|
||||
}}
|
||||
selectionMode="multiselect"
|
||||
onSelectionChange={updateVerboseOperations}
|
||||
>
|
||||
<DataGridHeader
|
||||
style={{
|
||||
paddingRight: scrollbarWidth,
|
||||
backgroundColor: "#e0e0e0",
|
||||
}}
|
||||
className={classes.gridHeader}
|
||||
>
|
||||
<DataGridRow>
|
||||
{({ renderHeaderCell }) => (
|
||||
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
|
||||
)}
|
||||
</DataGridRow>
|
||||
</DataGridHeader>
|
||||
<DataGridBody
|
||||
className={classes.gridBody}
|
||||
itemSize={40}
|
||||
height={gridHeight}
|
||||
>
|
||||
{({ item, rowId }, style) => {
|
||||
const isRowSelected =
|
||||
operationsState.selectedOperation?.id === (item as Item).id;
|
||||
const isFailed = (item as Item).status
|
||||
.toLowerCase()
|
||||
.includes("failed");
|
||||
const rowClassName =
|
||||
isRowSelected && isFailed
|
||||
? classes.selectedAndFailedRow
|
||||
: isFailed
|
||||
? classes.failedRow
|
||||
: isRowSelected
|
||||
? classes.selectedRow
|
||||
: classes.gridRow;
|
||||
|
||||
return (
|
||||
<DataGridRow<Item>
|
||||
key={rowId}
|
||||
style={style as React.CSSProperties}
|
||||
className={rowClassName}
|
||||
>
|
||||
{({ renderCell }) => {
|
||||
const cb = React.useCallback(() => onClick(item), [item]);
|
||||
return (
|
||||
<DataGridCell onClick={cb}>
|
||||
{renderCell(item as Item)}
|
||||
</DataGridCell>
|
||||
);
|
||||
}}
|
||||
</DataGridRow>
|
||||
);
|
||||
}}
|
||||
</DataGridBody>
|
||||
</DataGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import { makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
filterView: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflowY: "scroll",
|
||||
paddingLeft: "10px",
|
||||
backgroundColor: "#d6d6d6",
|
||||
paddingRight: "10px",
|
||||
height: "100%",
|
||||
"::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderBottom: "0.5px solid grey" as any,
|
||||
},
|
||||
type: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderBottom: "0.5px solid grey" as any,
|
||||
paddingBottom: "10px",
|
||||
},
|
||||
operationType: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderBottom: "0.5px solid grey" as any,
|
||||
paddingBottom: "10px",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,272 @@
|
|||
import * as React from "react";
|
||||
import { Checkbox } from "@fluentui/react-components";
|
||||
import { OperationType, ResultsFrom } from "apollo-inspector";
|
||||
import { useStyles } from "./filter-view.styles";
|
||||
|
||||
interface IFilterView {
|
||||
setFilters: (filterSet: IFilterSet | null) => void;
|
||||
}
|
||||
|
||||
export enum OperationStatus {
|
||||
InFlight = "InFlight",
|
||||
Succeded = "Succeded",
|
||||
Failed = "Failed",
|
||||
PartialSuccess = "PartialSuccess",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface IFilterSet {
|
||||
results: string[];
|
||||
types: string[];
|
||||
statuses: string[];
|
||||
}
|
||||
|
||||
export const querySubTypes = [
|
||||
OperationType.CacheReadQuery,
|
||||
OperationType.CacheWriteQuery,
|
||||
OperationType.ClientReadQuery,
|
||||
OperationType.ClientWriteQuery,
|
||||
];
|
||||
|
||||
export const fragmentSubTypes = [
|
||||
OperationType.CacheReadFragment,
|
||||
OperationType.CacheWriteFragment,
|
||||
OperationType.ClientReadFragment,
|
||||
OperationType.ClientWriteFragment,
|
||||
OperationType.Fragment,
|
||||
];
|
||||
|
||||
export const FilterView = React.memo((props: IFilterView) => {
|
||||
const [operationTypesFilter, setOperationTypesFilter] = React.useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [resultFromFilter, setResultFromFilter] = React.useState<string[]>([]);
|
||||
const [statusFilter, setStatusFilter] = React.useState<string[]>([]);
|
||||
const { setFilters } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const operationTypes = useOperationTypesCheckBox({
|
||||
operationTypesFilter,
|
||||
setOperationTypesFilter,
|
||||
setFilters,
|
||||
resultFromFilter,
|
||||
statusFilter,
|
||||
});
|
||||
|
||||
const onResultChange = useOnResultChange(
|
||||
resultFromFilter,
|
||||
setResultFromFilter,
|
||||
setFilters,
|
||||
operationTypesFilter,
|
||||
statusFilter,
|
||||
);
|
||||
const onStatusChange = useOnStatusChange(
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
setFilters,
|
||||
resultFromFilter,
|
||||
operationTypesFilter,
|
||||
);
|
||||
|
||||
const statues = Object.entries(OperationStatus)
|
||||
.filter((status) => status[0] !== OperationStatus.InFlight)
|
||||
.map((value, key) => {
|
||||
const checkboxValue = ((value as unknown) as Array<string>)[0];
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={onStatusChange}
|
||||
value={checkboxValue}
|
||||
label={checkboxValue}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const resultsFrom = Object.entries(ResultsFrom).map((value, key) => {
|
||||
const checkboxValue = ((value as unknown) as Array<string>)[0];
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={onResultChange}
|
||||
value={checkboxValue}
|
||||
label={checkboxValue}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className={classes.filterView}>
|
||||
<div>
|
||||
<div className={classes.filters}>
|
||||
<h3 key="operationType">{`Filters`} </h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.type}>
|
||||
<div>
|
||||
<h5 key="operationType">{`Type`} </h5>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{operationTypes}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.operationType}>
|
||||
<div>
|
||||
<h5 key="operationType">{`Result from`} </h5>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{resultsFrom}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div>
|
||||
<h5 key="operationType">{`Status`} </h5>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{statues}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface IUseOperationTypesCheckBoxParams {
|
||||
operationTypesFilter: string[];
|
||||
setOperationTypesFilter: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setFilters: (filterSet: IFilterSet | null) => void;
|
||||
resultFromFilter: string[];
|
||||
statusFilter: string[];
|
||||
}
|
||||
const useOperationTypesCheckBox = ({
|
||||
operationTypesFilter,
|
||||
setOperationTypesFilter,
|
||||
setFilters,
|
||||
resultFromFilter,
|
||||
statusFilter,
|
||||
}: IUseOperationTypesCheckBoxParams) => {
|
||||
const onOperationTypeChange = React.useCallback(
|
||||
({ target: { value } }, { checked }) => {
|
||||
let typesFilter = operationTypesFilter.concat([]);
|
||||
if (checked) {
|
||||
typesFilter.push(value);
|
||||
if (value == OperationType.Query) {
|
||||
querySubTypes.forEach((type) => {
|
||||
typesFilter.push(type);
|
||||
});
|
||||
|
||||
fragmentSubTypes.forEach((type) => {
|
||||
typesFilter.push(type);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
typesFilter = typesFilter.filter((x) => x !== value);
|
||||
|
||||
if (value == OperationType.Query) {
|
||||
typesFilter = typesFilter.filter((x) => {
|
||||
if (x === value) {
|
||||
return x === value;
|
||||
}
|
||||
if (querySubTypes.find((type) => type === x)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fragmentSubTypes.find((type) => type === x)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
setOperationTypesFilter(typesFilter);
|
||||
setTimeout(() => {
|
||||
setFilters({
|
||||
types: typesFilter,
|
||||
results: resultFromFilter,
|
||||
statuses: statusFilter,
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
[operationTypesFilter, setOperationTypesFilter, setFilters],
|
||||
);
|
||||
const operationTypes = React.useMemo(() => {
|
||||
return Object.entries(OperationType)
|
||||
.filter(
|
||||
(value) =>
|
||||
!querySubTypes.includes(
|
||||
((value as unknown) as Array<string>)[0] as OperationType,
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(value) =>
|
||||
!fragmentSubTypes.includes(
|
||||
((value as unknown) as Array<string>)[0] as OperationType,
|
||||
),
|
||||
)
|
||||
.map((value, key) => {
|
||||
const checkboxValue = ((value as unknown) as Array<string>)[0];
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={onOperationTypeChange}
|
||||
value={checkboxValue}
|
||||
label={checkboxValue}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [onOperationTypeChange]);
|
||||
|
||||
return operationTypes;
|
||||
};
|
||||
|
||||
const useOnStatusChange = (
|
||||
statusFilter: string[],
|
||||
setStatusFilter: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
setFilters: (filterSet: IFilterSet | null) => void,
|
||||
resultFromFilter: string[],
|
||||
operationTypesFilter: string[],
|
||||
) =>
|
||||
React.useCallback(
|
||||
({ target: { value } }, { checked }) => {
|
||||
let arr = statusFilter.concat([]);
|
||||
if (checked) {
|
||||
arr.push(value);
|
||||
} else {
|
||||
arr = arr.filter((x) => x !== value);
|
||||
}
|
||||
setStatusFilter(arr);
|
||||
setTimeout(() => {
|
||||
setFilters({
|
||||
results: resultFromFilter,
|
||||
types: operationTypesFilter,
|
||||
statuses: arr,
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
[statusFilter, setStatusFilter],
|
||||
);
|
||||
|
||||
const useOnResultChange = (
|
||||
resultFromFilter: string[],
|
||||
setResultFromFilter: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
setFilters: (filterSet: IFilterSet | null) => void,
|
||||
operationTypesFilter: string[],
|
||||
statusFilter: string[],
|
||||
) =>
|
||||
React.useCallback(
|
||||
({ target: { value } }, { checked }) => {
|
||||
let arr = resultFromFilter.concat([]);
|
||||
if (checked) {
|
||||
arr.push(value);
|
||||
} else {
|
||||
arr = arr.filter((x) => x !== value);
|
||||
}
|
||||
setResultFromFilter(arr);
|
||||
setTimeout(() => {
|
||||
setFilters({
|
||||
results: arr,
|
||||
types: operationTypesFilter,
|
||||
statuses: statusFilter,
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
[resultFromFilter, setResultFromFilter],
|
||||
);
|
|
@ -8,6 +8,9 @@ export const useStyles = makeStyles({
|
|||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexBasis: "auto",
|
||||
backgroundColor: "darkgrey",
|
||||
minWidth: 0,
|
||||
color: "black",
|
||||
},
|
||||
operationNameAccPanel: {
|
||||
whiteSpace: "pre-wrap",
|
||||
|
@ -36,6 +39,9 @@ export const useStyles = makeStyles({
|
|||
operationDetails: {
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
"::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
operationName: {
|
||||
display: "flex",
|
||||
|
@ -45,6 +51,25 @@ export const useStyles = makeStyles({
|
|||
minHeight: "32px",
|
||||
marginLeft: "1rem",
|
||||
},
|
||||
subHeading: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "32.5px",
|
||||
backgroundColor: "#e0e0e0",
|
||||
},
|
||||
|
||||
closeButton: {
|
||||
width: "40px",
|
||||
marginRight: "10px",
|
||||
},
|
||||
accordionWrapper: {
|
||||
minHeight: 0,
|
||||
overflowX: "scroll",
|
||||
},
|
||||
accordioPreWrapper: {
|
||||
maxWidth: "40rem",
|
||||
},
|
||||
});
|
||||
|
||||
export type stylesKeys =
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
AccordionPanel,
|
||||
Tooltip,
|
||||
Text,
|
||||
Button,
|
||||
Body1Strong,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
IVerboseOperation,
|
||||
|
@ -17,41 +19,84 @@ import { OperationVariables, WatchQueryFetchPolicy } from "@apollo/client";
|
|||
import {
|
||||
getOperationName,
|
||||
isNumber,
|
||||
sizeInBytes,
|
||||
} from "../utils/apollo-operations-tracker-utils";
|
||||
import { DocumentNode } from "graphql";
|
||||
import { ResultsFrom } from "../../../types";
|
||||
import {
|
||||
IOperationsAction as IOperationsReducerActions,
|
||||
OperationReducerActionEnum,
|
||||
} from "../operations-tracker-container-helper";
|
||||
|
||||
const spaceForStringify = 2;
|
||||
|
||||
interface IVerboseOperationViewProps {
|
||||
operation: IVerboseOperation | undefined;
|
||||
operation: IVerboseOperation | undefined | null;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsReducerActions>;
|
||||
}
|
||||
|
||||
export const VerboseOperationView = (props: IVerboseOperationViewProps) => {
|
||||
const classes = useStyles();
|
||||
const { operation } = props;
|
||||
const { operation, dispatchOperationsState } = props;
|
||||
if (!operation) {
|
||||
return <></>;
|
||||
}
|
||||
const classes = useStyles();
|
||||
|
||||
const { operationType } = operation;
|
||||
const { operationType, operationName } = operation;
|
||||
|
||||
const accordionItems = React.useMemo(
|
||||
() => getAccordionItems(operation, classes),
|
||||
[operation, classes],
|
||||
);
|
||||
|
||||
const closePreview = React.useCallback(() => {
|
||||
dispatchOperationsState({
|
||||
type: OperationReducerActionEnum.UpdateSelectedOperation,
|
||||
value: undefined,
|
||||
});
|
||||
}, [dispatchOperationsState]);
|
||||
|
||||
if (!operation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.operationView}>
|
||||
<h2 key="operationType">{operationType}</h2>
|
||||
<Accordion
|
||||
className={classes.operationDetails}
|
||||
key={"operationnViewAccordionn"}
|
||||
multiple
|
||||
collapsible
|
||||
<div className={classes.operationView} key="verboseOperationView">
|
||||
<div className={classes.subHeading} key="verboseHeader">
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
key="operationType1"
|
||||
>
|
||||
<h3 key="operationType">{`${operationType} : `} </h3>
|
||||
<Body1Strong underline key="operationName1ç">
|
||||
{operationName}
|
||||
</Body1Strong>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
className={classes.closeButton}
|
||||
onClick={closePreview}
|
||||
appearance="primary"
|
||||
key="closeButton"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classes.accordionWrapper}
|
||||
key="operationnViewAccordionnWrapper"
|
||||
>
|
||||
{...accordionItems}
|
||||
</Accordion>
|
||||
<div className={classes.accordioPreWrapper}>
|
||||
<Accordion
|
||||
className={classes.operationDetails}
|
||||
key={"operationnViewAccordionn"}
|
||||
multiple
|
||||
collapsible
|
||||
>
|
||||
{...accordionItems}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -95,7 +140,9 @@ const getOperationNamePanel = (
|
|||
) => {
|
||||
return (
|
||||
<AccordionItem value="operationName" key="operationName">
|
||||
<AccordionHeader>{operationName}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{operationName}</Text>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={classes.operationNameAccPanel}>{operationString}</div>
|
||||
</AccordionPanel>
|
||||
|
@ -109,7 +156,9 @@ const getVariablesPanel = (
|
|||
) => (
|
||||
<AccordionItem value="variables" key="variables">
|
||||
<Tooltip content={"Variables for the operation"} relationship="label">
|
||||
<AccordionHeader>{"Variables"}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{"Variables"}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.operationVariablesAccPanel}>
|
||||
|
@ -125,7 +174,11 @@ const getFetchPolicyPanel = (
|
|||
) => (
|
||||
<AccordionItem value="fetchPolicy" key="fetchPolicy">
|
||||
<Tooltip content={"Fetch policy of the operation"} relationship="label">
|
||||
<AccordionHeader>{"Fetch Policy"}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`Fetch Policy ${
|
||||
fetchPolicy ? `(${fetchPolicy})` : ``
|
||||
}`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.fetchPolicyAccPanel}> {fetchPolicy}</div>
|
||||
|
@ -150,7 +203,11 @@ const getAffectedQueriesPanel = (
|
|||
}
|
||||
relationship="label"
|
||||
>
|
||||
<AccordionHeader>{`Affected watch queries (${affectedQueriesItems.length})`}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text
|
||||
style={{ fontWeight: "bold" }}
|
||||
>{`Affected watch queries (${affectedQueriesItems.length})`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.affectedQueriesAccPanel}>
|
||||
|
@ -168,12 +225,16 @@ const getResultPanel = (
|
|||
result: IOperationResult[],
|
||||
classes: Record<stylesKeys, string>,
|
||||
) => {
|
||||
const items = result.map((res) => {
|
||||
const items = result.map((res: IOperationResult) => {
|
||||
const resultFrom = getResultFromString(res.from);
|
||||
|
||||
return (
|
||||
<AccordionItem value={resultFrom} key={resultFrom}>
|
||||
<AccordionHeader>{resultFrom}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`${resultFrom} ${
|
||||
res.size ? `(${sizeInBytes(res.size)})` : ``
|
||||
}`}</Text>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={classes.resultPanel}>
|
||||
{`${JSON.stringify(res.result, null, spaceForStringify)}`}
|
||||
|
@ -191,9 +252,11 @@ const getResultPanel = (
|
|||
}
|
||||
relationship="label"
|
||||
>
|
||||
<AccordionHeader>{`Result ${
|
||||
isOptimistic ? "(Optimistic result)" : ""
|
||||
}`}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`Result ${
|
||||
isOptimistic ? "(Optimistic result)" : ""
|
||||
}`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<Accordion collapsible>{...items}</Accordion>
|
||||
|
@ -220,7 +283,11 @@ const getErrorPanel = (error: unknown, classes: Record<stylesKeys, string>) => (
|
|||
content={"Error message for operation failure"}
|
||||
relationship="label"
|
||||
>
|
||||
<AccordionHeader>{`Error ${error ? "(failed)" : ""}`}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`Error ${
|
||||
error ? "(failed)" : ""
|
||||
}`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.errorAccPanel}> {JSON.stringify(error)}</div>
|
||||
|
@ -239,7 +306,9 @@ const getWarningPanel = (
|
|||
}
|
||||
relationship="label"
|
||||
>
|
||||
<AccordionHeader>{`Warning`}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`Warning`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.warningAccPanel}> {JSON.stringify(warning)}</div>
|
||||
|
@ -256,7 +325,11 @@ const getDurationPanel = (
|
|||
content={"Detailed time info for operation in milliSeconds"}
|
||||
relationship="label"
|
||||
>
|
||||
<AccordionHeader>{`Duration (ms)`}</AccordionHeader>
|
||||
<AccordionHeader>
|
||||
<Text style={{ fontWeight: "bold" }}>{`Duration ${
|
||||
duration?.totalTime ? `(${duration.totalTime} ms)` : `(ms)`
|
||||
}`}</Text>
|
||||
</AccordionHeader>
|
||||
</Tooltip>
|
||||
<AccordionPanel>
|
||||
<div className={classes.durationAccPanel}>
|
||||
|
|
|
@ -1,39 +1,47 @@
|
|||
import * as React from "react";
|
||||
import { IVerboseOperation } from "apollo-inspector";
|
||||
import { useStyles } from "./verbose-operations-list-view-styles";
|
||||
import { useStyles } from "./verbose-operations-container-styles";
|
||||
import { VerboseOperationView } from "./verbose-operation-view";
|
||||
import { VerboseOperationsListView } from "./verbose-operations-list-view";
|
||||
import { IReducerAction } from "../operations-tracker-body/operations-tracker-body.interface";
|
||||
import { DataGridView } from "./data-grid-view";
|
||||
import { ICountReducerAction } from "../operations-tracker-body/operations-tracker-body.interface";
|
||||
import {
|
||||
IOperationsAction,
|
||||
IOperationsReducerState,
|
||||
} from "../operations-tracker-container-helper";
|
||||
|
||||
export interface IVerboseOperationsContainerProps {
|
||||
operations: IVerboseOperation[] | null;
|
||||
filter: string;
|
||||
dispatchOperationsCount: React.Dispatch<IReducerAction>;
|
||||
operationsState: IOperationsReducerState;
|
||||
dispatchOperationsCount: React.Dispatch<ICountReducerAction>;
|
||||
dispatchOperationsState: React.Dispatch<IOperationsAction>;
|
||||
}
|
||||
|
||||
export const VerboseOperationsContainer = (
|
||||
props: IVerboseOperationsContainerProps,
|
||||
) => {
|
||||
const { operations, filter, dispatchOperationsCount } = props;
|
||||
const [selectedOperation, setSelectedOperation] = React.useState(
|
||||
props.operations?.[0],
|
||||
);
|
||||
const classes = useStyles();
|
||||
const {
|
||||
operations,
|
||||
operationsState,
|
||||
dispatchOperationsCount,
|
||||
dispatchOperationsState,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.operations}>
|
||||
<VerboseOperationsListView
|
||||
key={"VerboseOperationsListView"}
|
||||
<DataGridView
|
||||
key={"OperationsDataGridView"}
|
||||
operations={operations}
|
||||
filter={filter}
|
||||
setSelectedOperation={setSelectedOperation}
|
||||
selectedOperation={selectedOperation}
|
||||
operationsState={operationsState}
|
||||
dispatchOperationsCount={dispatchOperationsCount}
|
||||
dispatchOperationsState={dispatchOperationsState}
|
||||
/>
|
||||
|
||||
<VerboseOperationView
|
||||
key={"VerboseOperationView"}
|
||||
operation={selectedOperation}
|
||||
operation={operationsState.selectedOperation}
|
||||
dispatchOperationsState={dispatchOperationsState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { TabList, Tab, Text, Tooltip } from "@fluentui/react-components";
|
||||
import { IVerboseOperation } from "apollo-inspector";
|
||||
import { useStyles } from "./verbose-operations-list-view-styles";
|
||||
import {
|
||||
IReducerAction,
|
||||
ReducerActionEnum,
|
||||
} from "../operations-tracker-body/operations-tracker-body.interface";
|
||||
|
||||
export interface IVerboseOperationViewRendererProps {
|
||||
operations: IVerboseOperation[] | null;
|
||||
filter: string;
|
||||
setSelectedOperation: React.Dispatch<
|
||||
React.SetStateAction<IVerboseOperation | undefined>
|
||||
>;
|
||||
selectedOperation: IVerboseOperation | undefined;
|
||||
dispatchOperationsCount: React.Dispatch<IReducerAction>;
|
||||
}
|
||||
|
||||
export const VerboseOperationsListView = (
|
||||
props: IVerboseOperationViewRendererProps,
|
||||
) => {
|
||||
const {
|
||||
operations,
|
||||
filter,
|
||||
selectedOperation,
|
||||
setSelectedOperation,
|
||||
dispatchOperationsCount,
|
||||
} = props;
|
||||
|
||||
const [filteredOperations, setFilteredOperations] = React.useState<
|
||||
IVerboseOperation[] | null | undefined
|
||||
>(operations);
|
||||
|
||||
const classes = useStyles();
|
||||
const tabListItems = useOperationListNames(filteredOperations, classes);
|
||||
|
||||
const operationsMap = React.useMemo(() => {
|
||||
const map = new Map<number, IVerboseOperation>();
|
||||
filteredOperations?.forEach((op) => {
|
||||
map.set(op.id, op);
|
||||
});
|
||||
return map;
|
||||
}, [filteredOperations]);
|
||||
|
||||
const onTabSelect = React.useCallback(
|
||||
(_, props) => {
|
||||
const operation = operationsMap.get(props.value);
|
||||
setSelectedOperation(operation);
|
||||
},
|
||||
[setSelectedOperation, filteredOperations],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const filtereItems = getFilteredItems(operations, filter);
|
||||
setFilteredOperations(filtereItems);
|
||||
dispatchOperationsCount({
|
||||
type: ReducerActionEnum.UpdateVerboseOperationsCount,
|
||||
value: filtereItems?.length,
|
||||
});
|
||||
}, [filter, setFilteredOperations, operations, dispatchOperationsCount]);
|
||||
|
||||
return (
|
||||
<div className={classes.operationsNameListWrapper}>
|
||||
<TabList
|
||||
className={classes.operationsList}
|
||||
vertical
|
||||
selectedValue={selectedOperation?.id}
|
||||
onTabSelect={onTabSelect}
|
||||
key="operationNameList"
|
||||
>
|
||||
{tabListItems}
|
||||
</TabList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useOperationListNames = (
|
||||
filteredOperations: IVerboseOperation[] | null | undefined,
|
||||
classes: Record<
|
||||
| "operationName"
|
||||
| "root"
|
||||
| "operations"
|
||||
| "operationsList"
|
||||
| "operationNameWrapper"
|
||||
| "operationsNameListWrapper"
|
||||
| "opCountTxt"
|
||||
| "copyAllOpBtn",
|
||||
string
|
||||
>,
|
||||
) =>
|
||||
React.useMemo(() => {
|
||||
const tabItems = filteredOperations?.map((op) => {
|
||||
return (
|
||||
<Tab key={op.id} value={op.id}>
|
||||
<div className={classes.operationNameWrapper}>
|
||||
{(op.operationName?.length || 0) > 30 ? (
|
||||
<Tooltip content={op.operationName || ""} relationship="label">
|
||||
<Text className={classes.operationName} weight={"semibold"}>
|
||||
{op.operationName}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text className={classes.operationName} weight={"semibold"}>
|
||||
{op.operationName}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text>{`(${op.id}):${op.operationType}:${op.fetchPolicy}`}</Text>
|
||||
</div>
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
|
||||
return tabItems || [];
|
||||
}, [filteredOperations]);
|
||||
|
||||
const getFilteredItems = (
|
||||
items: IVerboseOperation[] | null | undefined,
|
||||
filter: string,
|
||||
) => {
|
||||
if (filter.length === 0) {
|
||||
return items;
|
||||
} else {
|
||||
const filteredItems = items?.filter((item) => {
|
||||
return (
|
||||
item.operationName?.toLowerCase().indexOf(filter.toLowerCase()) != -1
|
||||
);
|
||||
});
|
||||
return filteredItems;
|
||||
}
|
||||
};
|
|
@ -10,7 +10,7 @@
|
|||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"esbuild": "^0.14.38",
|
||||
"esbuild": "^0.17.12",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
|
1353
yarn.lock
1353
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче