mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-09-25 13:36:42 +03:00
Query actions and copy data to clipboard
- RTK query Refresh, Loading, Fulfill actions. - Copy button to copy json data.
This commit is contained in:
parent
68f82b6994
commit
193141bf52
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export type CopyIconProps = Omit<
|
||||||
|
HTMLAttributes<SVGElement>,
|
||||||
|
'xmlns' | 'children' | 'viewBox'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
/**
|
||||||
|
* @see import { IoCopySharp } from "react-icons/io5";
|
||||||
|
*/
|
||||||
|
export function CopyIcon(props: CopyIconProps): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
strokeWidth="0"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
height="1em"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M456 480H136a24 24 0 0 1-24-24V128a16 16 0 0 1 16-16h328a24 24 0 0 1 24 24v320a24 24 0 0 1-24 24z"></path>
|
||||||
|
<path d="M112 80h288V56a24 24 0 0 0-24-24H60a28 28 0 0 0-28 28v316a24 24 0 0 0 24 24h24V112a32 32 0 0 1 32-32z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable max-len */
|
|
@ -1,15 +1,19 @@
|
||||||
import { createSelector, Selector } from '@reduxjs/toolkit';
|
import { createSelector, Selector } from '@reduxjs/toolkit';
|
||||||
import React, { ReactNode, PureComponent } from 'react';
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
import { Action, AnyAction } from 'redux';
|
import { Action, AnyAction, Dispatch } from 'redux';
|
||||||
import type { KeyPath, ShouldExpandNodeInitially } from 'react-json-tree';
|
import type { KeyPath, ShouldExpandNodeInitially } from 'react-json-tree';
|
||||||
import { QueryPreviewTabs } from '../types';
|
import { QueryPreviewTabs, RtkResourceInfo } from '../types';
|
||||||
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
|
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
|
||||||
import { emptyRecord, identity } from '../utils/object';
|
import { emptyRecord, identity } from '../utils/object';
|
||||||
import { TreeView, TreeViewProps } from './TreeView';
|
import { TreeView, TreeViewProps } from './TreeView';
|
||||||
|
import { RTKActionButtons } from './RTKQueryActions';
|
||||||
|
|
||||||
export interface QueryPreviewActionsProps {
|
export interface QueryPreviewActionsProps {
|
||||||
isWideLayout: boolean;
|
isWideLayout: boolean;
|
||||||
actionsOfQuery: AnyAction[];
|
actionsOfQuery: AnyAction[];
|
||||||
|
dispatch?: Dispatch<AnyAction>;
|
||||||
|
resInfo?: RtkResourceInfo | null;
|
||||||
|
liftedDispatch?: Dispatch<AnyAction>; // Add lifted dispatch for DevTools
|
||||||
}
|
}
|
||||||
|
|
||||||
const keySep = ' - ';
|
const keySep = ' - ';
|
||||||
|
@ -79,15 +83,24 @@ export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps>
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
const { isWideLayout, actionsOfQuery } = this.props;
|
const { isWideLayout, actionsOfQuery, dispatch, liftedDispatch, resInfo } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeView
|
<>
|
||||||
rootProps={rootProps}
|
<RTKActionButtons
|
||||||
data={this.selectFormattedActions(actionsOfQuery)}
|
dispatch={dispatch}
|
||||||
isWideLayout={isWideLayout}
|
liftedDispatch={liftedDispatch}
|
||||||
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
|
resInfo={resInfo}
|
||||||
/>
|
actionsOfQuery={actionsOfQuery}
|
||||||
|
/>
|
||||||
|
<TreeView
|
||||||
|
rootProps={rootProps}
|
||||||
|
data={this.selectFormattedActions(actionsOfQuery)}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,296 @@
|
||||||
|
import { AnyAction, Dispatch } from 'redux';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Toolbar } from '@redux-devtools/ui';
|
||||||
|
import { RtkResourceInfo } from '../types';
|
||||||
|
|
||||||
|
interface RTKActionButtonsProps {
|
||||||
|
dispatch?: Dispatch<AnyAction>;
|
||||||
|
liftedDispatch?: Dispatch<AnyAction>;
|
||||||
|
resInfo?: RtkResourceInfo | null;
|
||||||
|
actionsOfQuery?: AnyAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_TYPES = {
|
||||||
|
REFRESH: 'REFRESH',
|
||||||
|
FULFILLED: 'FULFILLED',
|
||||||
|
TRIGGER_LOADING: 'TRIGGER_LOADING',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const createFulfilledMeta = (
|
||||||
|
requestStatus: string,
|
||||||
|
requestId: string,
|
||||||
|
baseEndpoint: string,
|
||||||
|
parsedArgs: any,
|
||||||
|
queryKey: string,
|
||||||
|
) => ({
|
||||||
|
arg: {
|
||||||
|
type: 'query',
|
||||||
|
subscribe: true,
|
||||||
|
forceRefetch: true,
|
||||||
|
subscriptionOptions: {
|
||||||
|
pollingInterval: 0,
|
||||||
|
skipPollingIfUnfocused: false,
|
||||||
|
},
|
||||||
|
endpointName: baseEndpoint,
|
||||||
|
originalArgs: parsedArgs || {},
|
||||||
|
queryCacheKey: queryKey,
|
||||||
|
},
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
fulfilledTimeStamp: Date.now(),
|
||||||
|
baseQueryMeta: {
|
||||||
|
request: {},
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
RTK_autoBatch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPendingMeta = (
|
||||||
|
requestStatus: string,
|
||||||
|
requestId: string,
|
||||||
|
baseEndpoint: string,
|
||||||
|
parsedArgs: any,
|
||||||
|
queryKey: string,
|
||||||
|
) => ({
|
||||||
|
arg: {
|
||||||
|
type: 'query',
|
||||||
|
subscribe: true,
|
||||||
|
forceRefetch: true,
|
||||||
|
subscriptionOptions: {
|
||||||
|
pollingInterval: 0,
|
||||||
|
skipPollingIfUnfocused: false,
|
||||||
|
},
|
||||||
|
endpointName: baseEndpoint,
|
||||||
|
originalArgs: parsedArgs || {},
|
||||||
|
queryCacheKey: queryKey,
|
||||||
|
},
|
||||||
|
requestId,
|
||||||
|
requestStatus,
|
||||||
|
baseQueryMeta: {
|
||||||
|
request: {},
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
RTK_autoBatch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRtkQueryActions = (
|
||||||
|
reducerPath: string,
|
||||||
|
queryKey: string,
|
||||||
|
endpointName?: string,
|
||||||
|
currentData?: any,
|
||||||
|
originalArgs?: any,
|
||||||
|
) => {
|
||||||
|
const baseEndpoint = endpointName || queryKey.split('(')[0];
|
||||||
|
let parsedArgs = originalArgs;
|
||||||
|
if (!parsedArgs && queryKey.includes('(')) {
|
||||||
|
const argsString = queryKey.split('(')[1]?.split(')')[0];
|
||||||
|
if (
|
||||||
|
argsString === undefined ||
|
||||||
|
argsString === '' ||
|
||||||
|
argsString === 'undefined'
|
||||||
|
) {
|
||||||
|
parsedArgs = undefined;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
parsedArgs = JSON.parse(argsString);
|
||||||
|
} catch (e) {
|
||||||
|
parsedArgs = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const actualPayload = currentData || {
|
||||||
|
message: `No data available for ${baseEndpoint}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
endpointName: baseEndpoint,
|
||||||
|
queryKey: queryKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh: {
|
||||||
|
type: `${reducerPath}/executeQuery/pending`,
|
||||||
|
payload: undefined,
|
||||||
|
meta: createPendingMeta(
|
||||||
|
'pending',
|
||||||
|
`refresh-${Date.now()}`,
|
||||||
|
baseEndpoint,
|
||||||
|
parsedArgs,
|
||||||
|
queryKey,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
fulfilled: {
|
||||||
|
type: `${reducerPath}/executeQuery/fulfilled`,
|
||||||
|
payload: actualPayload,
|
||||||
|
meta: createFulfilledMeta(
|
||||||
|
'fulfilled',
|
||||||
|
`fulfilled-${Date.now()}`,
|
||||||
|
baseEndpoint,
|
||||||
|
parsedArgs,
|
||||||
|
queryKey,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
triggerLoading: {
|
||||||
|
type: `${reducerPath}/executeQuery/pending`,
|
||||||
|
payload: undefined,
|
||||||
|
meta: createPendingMeta(
|
||||||
|
'pending',
|
||||||
|
`loading-${Date.now()}`,
|
||||||
|
baseEndpoint,
|
||||||
|
parsedArgs,
|
||||||
|
queryKey,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RTKActionButtons: React.FC<RTKActionButtonsProps> = ({
|
||||||
|
dispatch,
|
||||||
|
liftedDispatch,
|
||||||
|
resInfo,
|
||||||
|
actionsOfQuery,
|
||||||
|
}) => {
|
||||||
|
// Extract current data from query actions if available
|
||||||
|
let currentData: any;
|
||||||
|
if (actionsOfQuery && actionsOfQuery.length > 0) {
|
||||||
|
// Look for the most recent fulfilled action to get current data
|
||||||
|
const fulfilledActions = actionsOfQuery.filter(
|
||||||
|
(action) => action.type?.includes('/fulfilled') && action.payload,
|
||||||
|
);
|
||||||
|
if (fulfilledActions.length > 0) {
|
||||||
|
currentData = fulfilledActions[fulfilledActions.length - 1].payload;
|
||||||
|
console.log('📊 Found current data from actions:', currentData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resInfo) {
|
||||||
|
return (
|
||||||
|
<div css={{ padding: '8px 12px', fontSize: '12px', color: 'orange' }}>
|
||||||
|
No query selected
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let originalArgs;
|
||||||
|
let lastPendingAction;
|
||||||
|
|
||||||
|
if (actionsOfQuery && actionsOfQuery.length > 0) {
|
||||||
|
const queryAction = actionsOfQuery.find(
|
||||||
|
(action) =>
|
||||||
|
action.type?.includes('/executeQuery/') ||
|
||||||
|
action.type?.includes('/pending') ||
|
||||||
|
action.type?.includes('/fulfilled'),
|
||||||
|
);
|
||||||
|
if (queryAction?.meta?.arg?.originalArgs) {
|
||||||
|
originalArgs = queryAction.meta.arg.originalArgs;
|
||||||
|
}
|
||||||
|
const pendingActions = actionsOfQuery.filter(
|
||||||
|
(action) => action.type?.includes('/pending') && action.meta?.requestId,
|
||||||
|
);
|
||||||
|
if (pendingActions.length > 0) {
|
||||||
|
lastPendingAction = pendingActions[pendingActions.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = createRtkQueryActions(
|
||||||
|
resInfo.reducerPath,
|
||||||
|
resInfo.queryKey,
|
||||||
|
resInfo.queryKey.split('(')[0],
|
||||||
|
currentData,
|
||||||
|
originalArgs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastPendingAction) {
|
||||||
|
actions.fulfilled.meta.requestId = lastPendingAction.meta.requestId;
|
||||||
|
|
||||||
|
actions.fulfilled.meta.arg.queryCacheKey =
|
||||||
|
lastPendingAction.meta.arg.queryCacheKey;
|
||||||
|
|
||||||
|
actions.triggerLoading.meta.requestId = lastPendingAction.meta.requestId;
|
||||||
|
|
||||||
|
actions.triggerLoading.meta.arg.queryCacheKey =
|
||||||
|
lastPendingAction.meta.arg.queryCacheKey;
|
||||||
|
|
||||||
|
actions.refresh.meta.requestId = lastPendingAction.meta.requestId;
|
||||||
|
|
||||||
|
actions.refresh.meta.arg.queryCacheKey =
|
||||||
|
lastPendingAction.meta.arg.queryCacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAction = (
|
||||||
|
actionType: keyof typeof ACTION_TYPES,
|
||||||
|
action: AnyAction,
|
||||||
|
) => {
|
||||||
|
if (liftedDispatch) {
|
||||||
|
try {
|
||||||
|
const performAction = {
|
||||||
|
type: 'PERFORM_ACTION' as const,
|
||||||
|
action,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
liftedDispatch(performAction);
|
||||||
|
|
||||||
|
if (actionType === ACTION_TYPES.REFRESH && currentData) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const resolveAction = {
|
||||||
|
type: 'PERFORM_ACTION' as const,
|
||||||
|
action: actions.fulfilled,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
liftedDispatch(resolveAction);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in liftedDispatch:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
|
try {
|
||||||
|
dispatch(action);
|
||||||
|
if (actionType === ACTION_TYPES.REFRESH && currentData) {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(actions.fulfilled);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in dispatch:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toolbar borderPosition="bottom" style={{ alignItems: 'center' }}>
|
||||||
|
<label
|
||||||
|
css={(theme) => ({
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.TEXT_COLOR,
|
||||||
|
marginRight: '8px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
RTK Query Actions:
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
tooltipPosition="top-right"
|
||||||
|
onClick={() => handleAction(ACTION_TYPES.REFRESH, actions.refresh)}
|
||||||
|
title="Force refetch"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
tooltipPosition="top-right"
|
||||||
|
onClick={() =>
|
||||||
|
handleAction(ACTION_TYPES.TRIGGER_LOADING, actions.triggerLoading)
|
||||||
|
}
|
||||||
|
title="Simulate loading state"
|
||||||
|
>
|
||||||
|
Loading
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
tooltipPosition="top-right"
|
||||||
|
onClick={() => handleAction(ACTION_TYPES.FULFILLED, actions.fulfilled)}
|
||||||
|
title="Simulate successful response"
|
||||||
|
>
|
||||||
|
Fulfill
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import type { Interpolation, Theme } from '@emotion/react';
|
import type { Interpolation, Theme } from '@emotion/react';
|
||||||
|
import { Dispatch, AnyAction } from 'redux';
|
||||||
import {
|
import {
|
||||||
QueryPreviewTabs,
|
QueryPreviewTabs,
|
||||||
RtkResourceInfo,
|
RtkResourceInfo,
|
||||||
|
@ -61,6 +62,8 @@ export interface QueryPreviewProps<S = unknown> {
|
||||||
readonly isWideLayout: boolean;
|
readonly isWideLayout: boolean;
|
||||||
readonly selectorsSource: SelectorsSource<S>;
|
readonly selectorsSource: SelectorsSource<S>;
|
||||||
readonly selectors: InspectorSelectors<S>;
|
readonly selectors: InspectorSelectors<S>;
|
||||||
|
readonly dispatch?: Dispatch<AnyAction>;
|
||||||
|
readonly liftedDispatch?: Dispatch<AnyAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,9 +114,12 @@ const MappedApiPreview = mapProps<QueryPreviewTabProps, QueryPreviewApiProps>(
|
||||||
const MappedQueryPreviewActions = mapProps<
|
const MappedQueryPreviewActions = mapProps<
|
||||||
QueryPreviewTabProps,
|
QueryPreviewTabProps,
|
||||||
QueryPreviewActionsProps
|
QueryPreviewActionsProps
|
||||||
>(({ isWideLayout, selectorsSource, selectors }) => ({
|
>(({ isWideLayout, selectorsSource, selectors, resInfo, ...props }) => ({
|
||||||
isWideLayout,
|
isWideLayout,
|
||||||
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
|
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
|
||||||
|
dispatch: (props as any).dispatch,
|
||||||
|
liftedDispatch: (props as any).liftedDispatch,
|
||||||
|
resInfo,
|
||||||
}))(QueryPreviewActions);
|
}))(QueryPreviewActions);
|
||||||
|
|
||||||
const tabs: ReadonlyArray<
|
const tabs: ReadonlyArray<
|
||||||
|
|
|
@ -217,6 +217,8 @@ class RtkQueryInspector<S, A extends Action<string>> extends PureComponent<
|
||||||
onTabChange={this.handleTabChange}
|
onTabChange={this.handleTabChange}
|
||||||
isWideLayout={isWideLayout}
|
isWideLayout={isWideLayout}
|
||||||
hasNoApis={hasNoApi}
|
hasNoApis={hasNoApi}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
liftedDispatch={this.props.dispatch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,58 @@
|
||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import type { GetItemString, LabelRenderer } from 'react-json-tree';
|
import type { GetItemString, LabelRenderer } from 'react-json-tree';
|
||||||
import { isCollection, isIndexed, isKeyed } from 'immutable';
|
import { isCollection, isIndexed, isKeyed } from 'immutable';
|
||||||
import isIterable from '../utils/isIterable';
|
import isIterable from '../utils/isIterable';
|
||||||
import { DATA_TYPE_KEY } from '../monitor-config';
|
import { DATA_TYPE_KEY } from '../monitor-config';
|
||||||
|
import { Button, Toolbar } from '@redux-devtools/ui';
|
||||||
|
import { CopyIcon } from '../components/CopyIcon';
|
||||||
|
|
||||||
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
||||||
|
|
||||||
|
const copyToClipboard = async (data: any) => {
|
||||||
|
try {
|
||||||
|
const text =
|
||||||
|
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
console.log('Copied to clipboard');
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value =
|
||||||
|
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
console.log('Copied to clipboard');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CopyButton: React.FC<{ data: any }> = ({ data }) => {
|
||||||
|
const iconStyle: CSSProperties = {
|
||||||
|
width: '14px',
|
||||||
|
height: '14px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
noBorder
|
||||||
|
style={{
|
||||||
|
display: 'inline',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void copyToClipboard(data);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon style={iconStyle} />
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function isImmutable(value: unknown) {
|
function isImmutable(value: unknown) {
|
||||||
return isKeyed(value) || isIndexed(value) || isCollection(value);
|
return isKeyed(value) || isIndexed(value) || isCollection(value);
|
||||||
}
|
}
|
||||||
|
@ -79,6 +126,7 @@ export const getItemString: GetItemString = (type: string, data: any) => (
|
||||||
? `${data[DATA_TYPE_KEY] as string} `
|
? `${data[DATA_TYPE_KEY] as string} `
|
||||||
: ''}
|
: ''}
|
||||||
{getText(type, data, false, undefined)}
|
{getText(type, data, false, undefined)}
|
||||||
|
<CopyButton data={data} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user