mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: add Try it
button on table view to try operations from graphiql tab
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6016 Co-authored-by: Daniele Cammareri <5709409+dancamma@users.noreply.github.com> GitOrigin-RevId: a26d5c6e3b61a9f12f8e2ff476091a8dcef9e696
This commit is contained in:
parent
d03d86a5e7
commit
6028a93078
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FaEdit } from 'react-icons/fa';
|
||||
import styles from '../Common.module.scss';
|
||||
import { TryOperation } from './TryOperation';
|
||||
|
||||
class Heading extends React.Component {
|
||||
state = {
|
||||
@ -32,12 +33,25 @@ class Heading extends React.Component {
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { editable, currentValue, save, loading, property } = this.props;
|
||||
|
||||
const {
|
||||
editable,
|
||||
currentValue,
|
||||
save,
|
||||
loading,
|
||||
property,
|
||||
table,
|
||||
dispatch,
|
||||
source,
|
||||
} = this.props;
|
||||
const { text, isEditting } = this.state;
|
||||
|
||||
if (!editable) {
|
||||
return <h2 className={styles.heading_text}>{currentValue}</h2>;
|
||||
return (
|
||||
<div className={styles.editable_heading_text}>
|
||||
<h2>{currentValue}</h2>
|
||||
<TryOperation table={table} dispatch={dispatch} source={source} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!save) {
|
||||
@ -48,6 +62,7 @@ class Heading extends React.Component {
|
||||
return (
|
||||
<div className={styles.editable_heading_text}>
|
||||
<h2>{currentValue}</h2>
|
||||
<TryOperation table={table} dispatch={dispatch} source={source} />
|
||||
<div
|
||||
onClick={this.toggleEditting}
|
||||
className={styles.editable_heading_action}
|
||||
|
140
console/src/components/Common/EditableHeading/TryOperation.tsx
Normal file
140
console/src/components/Common/EditableHeading/TryOperation.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Dispatch } from 'redux';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Badge } from '@/new-components/Badge';
|
||||
import { DropdownButton } from '@/new-components/DropdownButton';
|
||||
import { FaArrowRight, FaFlask } from 'react-icons/fa';
|
||||
import { LS_KEYS, setLSItem } from '@/utils/localStorage';
|
||||
import { Tooltip } from '@/new-components/Tooltip';
|
||||
import { useMetadataSource } from '@/features/MetadataAPI';
|
||||
import {
|
||||
getInitialValueField,
|
||||
generateGqlQueryFromTable,
|
||||
OperationType,
|
||||
} from './utils';
|
||||
import { Table } from '../../../dataSources/types';
|
||||
import _push from '../../Services/Data/push';
|
||||
|
||||
type Props = {
|
||||
table: Table;
|
||||
dispatch: Dispatch;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export const TryOperation = (props: Props) => {
|
||||
const { table, dispatch, source } = props;
|
||||
const { data: dbInfo } = useMetadataSource(source);
|
||||
const tryOnGQL = (queryType: OperationType) => () => {
|
||||
const { query, variables } = generateGqlQueryFromTable(queryType, table);
|
||||
setLSItem(LS_KEYS.graphiqlQuery, query);
|
||||
setLSItem(LS_KEYS.graphiqlVariables, JSON.stringify(variables));
|
||||
dispatch(_push('/api/api-explorer'));
|
||||
};
|
||||
|
||||
const isStreamingSubscriptionAvailable = !!getInitialValueField(table);
|
||||
const isTryitButtonAvailable =
|
||||
isEmpty(table?.configuration) &&
|
||||
isEmpty(dbInfo?.customization?.root_fields) &&
|
||||
isEmpty(dbInfo?.customization?.type_names) &&
|
||||
dbInfo?.customization?.naming_convention !== 'graphql-default';
|
||||
|
||||
const streamingSubscripton = (
|
||||
<div
|
||||
onClick={
|
||||
isStreamingSubscriptionAvailable
|
||||
? tryOnGQL('streaming_subscription')
|
||||
: undefined
|
||||
}
|
||||
data-trackid="data-tab-btn-try-streaming-subscriptions"
|
||||
className={clsx(
|
||||
'py-xs w-full flex justify-between align-center',
|
||||
isStreamingSubscriptionAvailable
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed text-muted'
|
||||
)}
|
||||
>
|
||||
Streaming Subscription
|
||||
<Badge className="mx-2" color="yellow">
|
||||
New
|
||||
</Badge>
|
||||
<div className="text-muted">
|
||||
<FaArrowRight className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (isTryitButtonAvailable) {
|
||||
return (
|
||||
<DropdownButton
|
||||
items={[
|
||||
[
|
||||
<span className="py-xs text-xs font-semibold text-muted uppercase tracking-wider whitespace-nowrap">
|
||||
Try it in GraphiQL
|
||||
</span>,
|
||||
<div
|
||||
onClick={tryOnGQL('query')}
|
||||
className="py-xs w-full flex justify-between align-center"
|
||||
data-trackid="data-tab-btn-try-query"
|
||||
>
|
||||
Query
|
||||
<div className="text-muted">
|
||||
<FaArrowRight className="w-3 h-3" />
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
onClick={tryOnGQL('mutation')}
|
||||
className="py-xs w-full flex justify-between align-center"
|
||||
data-trackid="data-tab-btn-try-streaming-mutation"
|
||||
>
|
||||
Mutation
|
||||
<div className="text-muted">
|
||||
<FaArrowRight className="w-3 h-3" />
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
onClick={tryOnGQL('subscription')}
|
||||
className="py-xs w-full flex justify-between align-center"
|
||||
data-trackid="data-tab-btn-try-subscriptions"
|
||||
>
|
||||
Subscription
|
||||
<div className="text-muted">
|
||||
<FaArrowRight className="w-3 h-3" />
|
||||
</div>
|
||||
</div>,
|
||||
isStreamingSubscriptionAvailable ? (
|
||||
streamingSubscripton
|
||||
) : (
|
||||
<Tooltip
|
||||
className="ml-0"
|
||||
tooltipContentChildren="Streaming subscriptions are not available for this table because there is no valid field to use as the initial value."
|
||||
>
|
||||
{streamingSubscripton}
|
||||
</Tooltip>
|
||||
),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<FaFlask className="mr-xs" />
|
||||
Try it
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
className="ml-0"
|
||||
tooltipContentChildren='"Try it" is disabled for this table because it has customizations. Head to API Explorer to try out a query.'
|
||||
>
|
||||
<DropdownButton
|
||||
items={[]}
|
||||
className={clsx(
|
||||
'py-xs w-full flex justify-between align-center cursor-not-allowed text-muted'
|
||||
)}
|
||||
onClick={undefined}
|
||||
>
|
||||
<FaFlask className="mr-xs" />
|
||||
Try it
|
||||
</DropdownButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -0,0 +1,150 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { generateGqlQueryFromTable } from './utils';
|
||||
import { handlers } from '../../../features/PermissionsForm/mocks/handlers.mock';
|
||||
import { Table } from '../../../dataSources/types';
|
||||
|
||||
const table: Table = {
|
||||
table_name: 'test',
|
||||
table_schema: 'public',
|
||||
table_type: 'TABLE',
|
||||
columns: [
|
||||
{
|
||||
column_default: "nextval('test_id_seq'::regclass)",
|
||||
column_name: 'id',
|
||||
comment: null,
|
||||
data_type: 'integer',
|
||||
data_type_name: 'int4',
|
||||
identity_generation: null,
|
||||
is_generated: false,
|
||||
is_identity: false,
|
||||
is_nullable: 'NO',
|
||||
ordinal_position: 1,
|
||||
table_name: 'test',
|
||||
table_schema: 'public',
|
||||
},
|
||||
{
|
||||
column_default: null,
|
||||
column_name: 'name',
|
||||
comment: null,
|
||||
data_type: 'text',
|
||||
data_type_name: 'text',
|
||||
identity_generation: null,
|
||||
is_generated: false,
|
||||
is_identity: false,
|
||||
is_nullable: 'YES',
|
||||
ordinal_position: 2,
|
||||
table_name: 'test',
|
||||
table_schema: 'public',
|
||||
},
|
||||
],
|
||||
comment: null,
|
||||
primary_key: {
|
||||
columns: ['id'],
|
||||
constraint_name: 'test_pkey',
|
||||
table_name: 'test',
|
||||
table_schema: 'public',
|
||||
},
|
||||
is_table_tracked: true,
|
||||
relationships: [],
|
||||
remote_relationships: [],
|
||||
view_info: null,
|
||||
unique_constraints: [],
|
||||
permissions: [],
|
||||
opp_foreign_key_constraints: [],
|
||||
foreign_key_constraints: [],
|
||||
check_constraints: [],
|
||||
computed_fields: [],
|
||||
is_enum: false,
|
||||
};
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('generateGqlQueryFromTable', () => {
|
||||
beforeEach(() => {
|
||||
server.use(...handlers());
|
||||
});
|
||||
|
||||
test('When generateGqlQueryFromTable is used with a query and table Then it should genrate a operation', async () => {
|
||||
const operationType = 'query';
|
||||
const { query, variables } = generateGqlQueryFromTable(
|
||||
operationType,
|
||||
table
|
||||
);
|
||||
|
||||
// need snapshot to exactly match the query
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
"query GetTest {
|
||||
test {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(variables).toBe(undefined);
|
||||
});
|
||||
test('When generateGqlQueryFromTable is used with a mutation and table Then it should genrate a operation', async () => {
|
||||
const operationType = 'mutation';
|
||||
const { query, variables } = generateGqlQueryFromTable(
|
||||
operationType,
|
||||
table
|
||||
);
|
||||
|
||||
// need snapshot to exactly match the mutation
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
"mutation InsertTest($name: String) {
|
||||
insert_test(objects: {name: $name}) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(variables).toEqual({ name: '' });
|
||||
});
|
||||
test('When generateGqlQueryFromTable is used with a streaming subscription and table Then it should genrate a operation', async () => {
|
||||
const operationType = 'streaming_subscription';
|
||||
const { query, variables } = generateGqlQueryFromTable(
|
||||
operationType,
|
||||
table
|
||||
);
|
||||
|
||||
// need snapshot to exactly match the streaming_subscription
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
"subscription GetTestStreamingSubscription {
|
||||
test_stream(batch_size: 10, cursor: {initial_value: {id: 0}}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(variables).toBe(undefined);
|
||||
});
|
||||
test('When generateGqlQueryFromTable is used with a subscription and table Then it should genrate a operation', async () => {
|
||||
const operationType = 'subscription';
|
||||
const { query, variables } = generateGqlQueryFromTable(
|
||||
operationType,
|
||||
table
|
||||
);
|
||||
|
||||
// need snapshot to exactly match the subscription
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
"subscription GetTestStreamingSubscription {
|
||||
test {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
"
|
||||
`);
|
||||
expect(variables).toBe(undefined);
|
||||
});
|
||||
});
|
177
console/src/components/Common/EditableHeading/utils.ts
Normal file
177
console/src/components/Common/EditableHeading/utils.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { Table, TableColumn } from '@/dataSources/types';
|
||||
import moment from 'moment';
|
||||
import { capitaliseFirstLetter } from '../ConfigureTransformation/utils';
|
||||
|
||||
const indentFields = (subFields: TableColumn[], depth: number) =>
|
||||
subFields
|
||||
.map(sf => sf.column_name)
|
||||
.join(`\n${Array(depth).fill('\t').join('')}`)
|
||||
.trim();
|
||||
|
||||
const toPascalCase = (str: string) =>
|
||||
str.split(/[-_]/).map(capitaliseFirstLetter).join('');
|
||||
|
||||
const subFieldsTypeMapping: Record<
|
||||
string,
|
||||
{ type: string; default: () => unknown }
|
||||
> = {
|
||||
integer: {
|
||||
type: 'Int',
|
||||
default: () => 0,
|
||||
},
|
||||
text: {
|
||||
type: 'String',
|
||||
default: () => '',
|
||||
},
|
||||
boolean: {
|
||||
type: 'Boolean',
|
||||
default: () => true,
|
||||
},
|
||||
numeric: {
|
||||
type: 'Float',
|
||||
default: () => 0,
|
||||
},
|
||||
|
||||
'timestamp with time zone': {
|
||||
type: 'String',
|
||||
default: () => moment().format(),
|
||||
},
|
||||
'time with time zone': {
|
||||
type: 'String',
|
||||
default: () => moment().format('HH:mm:ss.SSSSSSZZ'),
|
||||
},
|
||||
date: {
|
||||
type: 'String',
|
||||
default: () => moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
uuid: {
|
||||
type: 'String',
|
||||
default: () => '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
jsonb: {
|
||||
type: 'String',
|
||||
default: () => '{}',
|
||||
},
|
||||
bigint: {
|
||||
type: 'Int',
|
||||
default: () => 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const getInitialValueField = (table: Table): TableColumn | undefined =>
|
||||
table.columns.find(
|
||||
f =>
|
||||
(table.primary_key?.columns || []).includes(f.column_name) &&
|
||||
['bigint', 'integer'].includes(f.data_type)
|
||||
) ??
|
||||
table.columns.find(f =>
|
||||
['timestamp with time zone', 'time with time zone', 'date'].includes(
|
||||
f.data_type
|
||||
)
|
||||
);
|
||||
|
||||
export type OperationType =
|
||||
| 'subscription'
|
||||
| 'query'
|
||||
| 'mutation'
|
||||
| 'streaming_subscription';
|
||||
|
||||
export const generateGqlQueryFromTable = (
|
||||
operationType: OperationType = 'subscription',
|
||||
table: Table
|
||||
): {
|
||||
query: string;
|
||||
variables?: Record<string, unknown>;
|
||||
} => {
|
||||
const fieldName = table.table_name;
|
||||
const fields = table.columns;
|
||||
const pascalCaseName = toPascalCase(fieldName);
|
||||
|
||||
if (operationType === 'query') {
|
||||
const query = `query Get${pascalCaseName} {
|
||||
${fieldName} {
|
||||
${indentFields(fields, 2)}
|
||||
}
|
||||
}
|
||||
`;
|
||||
return {
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
if (operationType === 'mutation') {
|
||||
const mandatoryFields = fields.filter(
|
||||
sf => !sf.column_default && subFieldsTypeMapping[sf.data_type]
|
||||
);
|
||||
const args = mandatoryFields
|
||||
.map(
|
||||
sf => `$${sf.column_name}: ${subFieldsTypeMapping[sf.data_type].type}`
|
||||
)
|
||||
.join(', ')
|
||||
.trim();
|
||||
const argsUsage = mandatoryFields
|
||||
.map(sf => `${sf.column_name}: $${sf.column_name}`)
|
||||
.join(', ')
|
||||
.trim();
|
||||
const query = `mutation Insert${pascalCaseName}(${args}) {
|
||||
insert_${fieldName}(objects: {${argsUsage}}) {
|
||||
affected_rows
|
||||
returning {
|
||||
${indentFields(fields, 3)}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = mandatoryFields.reduce(
|
||||
(acc, sf) => ({
|
||||
...acc,
|
||||
[sf.column_name]: subFieldsTypeMapping[sf.data_type].default(),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
query,
|
||||
variables,
|
||||
};
|
||||
}
|
||||
|
||||
if (operationType === 'streaming_subscription') {
|
||||
const initialValueColumn = getInitialValueField(table);
|
||||
if (initialValueColumn) {
|
||||
const initialValue = JSON.stringify(
|
||||
subFieldsTypeMapping[initialValueColumn.data_type]?.default()
|
||||
);
|
||||
|
||||
const query = `subscription Get${pascalCaseName}StreamingSubscription {
|
||||
${fieldName}_stream(batch_size: 10, cursor: {initial_value: {${
|
||||
initialValueColumn.column_name
|
||||
}: ${initialValue}}}) {
|
||||
${indentFields(fields, 2)}
|
||||
}
|
||||
}
|
||||
`;
|
||||
return {
|
||||
query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (operationType === 'subscription') {
|
||||
const query = `subscription Get${pascalCaseName}StreamingSubscription {
|
||||
${fieldName} {
|
||||
${indentFields(fields, 2)}
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
return {
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query: '',
|
||||
};
|
||||
};
|
@ -105,6 +105,8 @@ const TableHeader = ({
|
||||
}
|
||||
dispatch={dispatch}
|
||||
property={isTableType ? 'table' : 'view'}
|
||||
table={table}
|
||||
source={source}
|
||||
/>
|
||||
<div className={styles.nav}>
|
||||
<ul className="nav nav-pills">
|
||||
|
@ -97,7 +97,7 @@ export const AutoCleanupForm = (props: AutoCleanupFormProps) => {
|
||||
schedule: cron.value,
|
||||
});
|
||||
}}
|
||||
className="cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100"
|
||||
className="py-xs cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100"
|
||||
>
|
||||
<p className="mb-0 font-semibold whitespace-nowrap">
|
||||
{cron.label}
|
||||
|
@ -93,7 +93,7 @@ export const ManageTable = (props: ManageTableProps) => {
|
||||
items={[
|
||||
[
|
||||
// TODO: To be implemented after metadata util functions have been added to the metadata library
|
||||
<span className="text-red-600" onClick={() => {}}>
|
||||
<span className="py-xs text-red-600" onClick={() => {}}>
|
||||
Untrack {tableName}
|
||||
</span>,
|
||||
],
|
||||
|
@ -37,7 +37,7 @@ export const QueryCollectionHeaderMenu: React.FC<QueryCollectionHeaderMenuProps>
|
||||
items={[
|
||||
[
|
||||
<div
|
||||
className="font-semibold"
|
||||
className="py-xs font-semibold"
|
||||
onClick={() => {
|
||||
// this is a workaround for a weird but caused by interaction of radix ui dialog and dropdown menu
|
||||
setTimeout(() => {
|
||||
@ -51,7 +51,7 @@ export const QueryCollectionHeaderMenu: React.FC<QueryCollectionHeaderMenuProps>
|
||||
entry => entry.collection === queryCollection.name
|
||||
) ? (
|
||||
<div
|
||||
className="font-semibold"
|
||||
className="py-xs font-semibold"
|
||||
onClick={() => {
|
||||
removeFromAllowList(queryCollection.name, {
|
||||
onSuccess: () => {
|
||||
@ -101,7 +101,7 @@ export const QueryCollectionHeaderMenu: React.FC<QueryCollectionHeaderMenuProps>
|
||||
],
|
||||
[
|
||||
<div
|
||||
className="font-semibold text-red-600"
|
||||
className="py-xs font-semibold text-red-600"
|
||||
onClick={() => {
|
||||
const confirmMessage = `This will permanently delete the query collection "${queryCollection.name}"`;
|
||||
const isOk = getConfirmation(
|
||||
|
@ -138,7 +138,7 @@ export const QuickAdd = (props: QuickAddProps) => {
|
||||
<div
|
||||
key={operation.name}
|
||||
onClick={() => onAdd(operation)}
|
||||
className="cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100"
|
||||
className="cursor-pointer mx-1 px-xs py-xs rounded hover:bg-gray-100"
|
||||
>
|
||||
<p className="mb-0 font-semibold whitespace-nowrap">
|
||||
{operation.name}
|
||||
|
@ -62,6 +62,7 @@ export const QueryCollectionsOperationsHeader: React.FC<QueryCollectionsOperatio
|
||||
items={[
|
||||
otherCollections.map(collection => (
|
||||
<div
|
||||
className="py-xs"
|
||||
onClick={() =>
|
||||
moveOperationToQueryCollection(
|
||||
collectionName,
|
||||
@ -105,6 +106,7 @@ export const QueryCollectionsOperationsHeader: React.FC<QueryCollectionsOperatio
|
||||
items={[
|
||||
otherCollections.map(collection => (
|
||||
<div
|
||||
className="py-xs"
|
||||
data-testid={`add-to-${collection.name}`}
|
||||
onClick={() =>
|
||||
addOperationToQueryCollection(
|
||||
|
@ -18,6 +18,7 @@ export const DropdownButton: React.FC<DropdownButtonProps> = ({
|
||||
<FaChevronDown className="transition-transform group-radix-state-open:rotate-180 w-3 h-3" />
|
||||
}
|
||||
{...rest}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
@ -63,15 +63,21 @@ export const DropdownMenu: React.FC<DropdownMenuProps> = ({
|
||||
<DropdownMenuPrimitive.Root {...options?.root}>
|
||||
<DropdownMenuTrigger {...options?.trigger}>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuPrimitive.Portal {...options?.portal}>
|
||||
<DropdownMenuContent {...options?.content}>
|
||||
{items.map(group => (
|
||||
<DropdownMenuPrimitive.Group className="">
|
||||
{group.map(item => (
|
||||
<DropdownMenuItem {...options?.item}>{item}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuPrimitive.Group>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
<DropdownMenuPrimitive.Content align="start" {...options?.content}>
|
||||
<div className="origin-top-left absolute left-0 z-10 mt-xs w-max max-w-xs rounded shadow-md bg-white ring-1 ring-gray-300 divide-y divide-gray-300 focus:outline-none">
|
||||
{items.map(group => (
|
||||
<div className="py-1">
|
||||
{group.map(item => (
|
||||
<DropdownMenuPrimitive.Item {...options?.item}>
|
||||
<div className="cursor-pointer flex items-center mx-1 px-xs rounded whitespace-nowrap hover:bg-gray-100">
|
||||
{item}
|
||||
</div>
|
||||
</DropdownMenuPrimitive.Item>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
);
|
||||
|
@ -88,6 +88,7 @@ export const LS_KEYS = {
|
||||
dataPageSizeKey: 'data:pageSize',
|
||||
derivedActions: 'actions:derivedActions',
|
||||
graphiqlQuery: 'graphiql:query',
|
||||
graphiqlVariables: 'graphiql:variables',
|
||||
loveConsent: 'console:loveIcon',
|
||||
oneGraphExplorerCodeExporterOpen: 'graphiql:codeExporterOpen',
|
||||
oneGraphExplorerOpen: 'graphiql:explorerOpen',
|
||||
|
Loading…
Reference in New Issue
Block a user