Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* text=auto eol=lf
* text=auto eol=lf
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ coverage
.idea
.eslintcache
!packages/redux-devtools-slider-monitor/examples/todomvc/dist/index.html
.nx
.nx
Original file line number Diff line number Diff line change
@@ -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 */
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { createSelector, Selector } from '@reduxjs/toolkit';
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 { QueryPreviewTabs } from '../types';
import { QueryPreviewTabs, RtkResourceInfo } from '../types';
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { emptyRecord, identity } from '../utils/object';
import { TreeView, TreeViewProps } from './TreeView';
import { RTKActionButtons } from './RTKQueryActions';

export interface QueryPreviewActionsProps {
isWideLayout: boolean;
actionsOfQuery: AnyAction[];
dispatch?: Dispatch<AnyAction>;
resInfo?: RtkResourceInfo | null;
liftedDispatch?: Dispatch<AnyAction>; // Add lifted dispatch for DevTools
}

const keySep = ' - ';
Expand Down Expand Up @@ -79,15 +83,23 @@ export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps>
};

render(): ReactNode {
const { isWideLayout, actionsOfQuery } = this.props;
const { isWideLayout, actionsOfQuery, dispatch, liftedDispatch, resInfo } =
this.props;

return (
<TreeView
rootProps={rootProps}
data={this.selectFormattedActions(actionsOfQuery)}
isWideLayout={isWideLayout}
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
/>
<>
<RTKActionButtons
dispatch={dispatch}
liftedDispatch={liftedDispatch}
data={this.selectFormattedActions(actionsOfQuery)}
/>
<TreeView
rootProps={rootProps}
data={this.selectFormattedActions(actionsOfQuery)}
isWideLayout={isWideLayout}
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
/>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Button, Toolbar } from '@redux-devtools/ui';
import React from 'react';
import { AnyAction, Dispatch } from 'redux';

interface RTKActionButtonsProps {
dispatch?: Dispatch<AnyAction>;
liftedDispatch?: Dispatch<AnyAction>;
data: Record<string, AnyAction>;
}

const ACTION_TYPES = {
REFRESH: 'REFRESH',
FULFILLED: 'FULFILLED',
TRIGGER_FETCHING: 'TRIGGER_FETCHING',
} as const;

const REFRESH_TIMEOUT_MS = 1000;

export const RTKActionButtons: React.FC<RTKActionButtonsProps> = ({
dispatch,
liftedDispatch,
data,
}) => {
const [isLoading, setIsLoading] = React.useState(false);
const pending = Object.values(data).filter(
(action) => action.meta?.requestStatus === 'pending',
);

const fulfilled = Object.values(data).filter(
(action) => action.meta?.requestStatus === 'fulfilled',
);

// Get the most recent actions
const recentPending = pending[pending.length - 1];
const recentFulfilled = fulfilled[fulfilled.length - 1];

const handleAction = (
actionType: keyof typeof ACTION_TYPES,
action: AnyAction,
fulfilledAction?: AnyAction,
) => {
setIsLoading(true);
if (liftedDispatch) {
try {
const performAction = {
type: 'PERFORM_ACTION' as const,
action,
timestamp: Date.now(),
};
liftedDispatch(performAction);

// For refresh actions, automatically dispatch fulfilled action after delay
if (actionType === ACTION_TYPES.REFRESH && fulfilledAction) {
setTimeout(() => {
const resolveAction = {
type: 'PERFORM_ACTION' as const,
action: fulfilledAction,
timestamp: Date.now(),
};
liftedDispatch(resolveAction);
setIsLoading(false);
}, REFRESH_TIMEOUT_MS);
}
} catch (error) {
console.error('Error in liftedDispatch:', error);
}
}

if (dispatch) {
try {
dispatch(action);

// For refresh actions, automatically dispatch fulfilled action after delay
if (actionType === ACTION_TYPES.REFRESH && fulfilledAction) {
setTimeout(() => {
dispatch(fulfilledAction);
setIsLoading(false);
}, REFRESH_TIMEOUT_MS);
}
} catch (error) {
console.error('Error in dispatch:', error);
}
}
};

return (
<Toolbar
borderPosition="bottom"
style={{ alignItems: 'center' }}
key={JSON.stringify(data)}
>
<label
css={(theme) => ({
fontSize: '12px',
fontWeight: 'bold',
color: theme.TEXT_COLOR,
marginRight: '8px',
})}
>
RTK Query Actions:
</label>

<Button
mark={isLoading && 'base0D'}
onClick={() =>
handleAction(ACTION_TYPES.REFRESH, recentPending, recentFulfilled)
}
disabled={isLoading || !recentPending || !recentFulfilled}
>
Refresh
</Button>

<Button
mark={isLoading && 'base0D'}
onClick={() =>
handleAction(ACTION_TYPES.TRIGGER_FETCHING, recentPending)
}
disabled={isLoading || !recentPending || !recentFulfilled}
>
Fetching
</Button>

<Button
onClick={() => {
handleAction(ACTION_TYPES.FULFILLED, recentFulfilled);
setIsLoading(false);
}}
disabled={!recentFulfilled}
>
Fulfill
</Button>
</Toolbar>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ReactNode } from 'react';
import type { Interpolation, Theme } from '@emotion/react';
import { Dispatch, AnyAction } from 'redux';
import {
QueryPreviewTabs,
RtkResourceInfo,
Expand Down Expand Up @@ -61,6 +62,8 @@ export interface QueryPreviewProps<S = unknown> {
readonly isWideLayout: boolean;
readonly selectorsSource: SelectorsSource<S>;
readonly selectors: InspectorSelectors<S>;
readonly dispatch?: Dispatch<AnyAction>;
readonly liftedDispatch?: Dispatch<AnyAction>;
}

/**
Expand Down Expand Up @@ -111,9 +114,12 @@ const MappedApiPreview = mapProps<QueryPreviewTabProps, QueryPreviewApiProps>(
const MappedQueryPreviewActions = mapProps<
QueryPreviewTabProps,
QueryPreviewActionsProps
>(({ isWideLayout, selectorsSource, selectors }) => ({
>(({ isWideLayout, selectorsSource, selectors, resInfo, ...props }) => ({
isWideLayout,
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
dispatch: (props as any).dispatch,
liftedDispatch: (props as any).liftedDispatch,
resInfo,
}))(QueryPreviewActions);

const tabs: ReadonlyArray<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ class RtkQueryInspector<S, A extends Action<string>> extends PureComponent<
onTabChange={this.handleTabChange}
isWideLayout={isWideLayout}
hasNoApis={hasNoApi}
dispatch={this.props.dispatch}
liftedDispatch={this.props.dispatch}
/>
</div>
);
Expand Down
50 changes: 49 additions & 1 deletion packages/redux-devtools-rtk-query-monitor/src/styles/tree.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import type { GetItemString, LabelRenderer } from 'react-json-tree';
import { isCollection, isIndexed, isKeyed } from 'immutable';
import isIterable from '../utils/isIterable';
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 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) {
return isKeyed(value) || isIndexed(value) || isCollection(value);
}
Expand Down Expand Up @@ -79,6 +126,7 @@ export const getItemString: GetItemString = (type: string, data: any) => (
? `${data[DATA_TYPE_KEY] as string} `
: ''}
{getText(type, data, false, undefined)}
<CopyButton data={data} />
</span>
);

Expand Down