mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
pro-console: Make caching discoverable on console
### Description Since caching is now enabled for free tier, the following changes are to be done to ensure caching is more discoverable: Spec: https://docs.google.com/document/d/1Ic2I7wQYj_A8qJhRffboQPdDsozd463fy-wuvb1bERw/edit ### Affected components - [x] Console ### Related Issues #2340 ### Solution and Design 1. Added a new Cache button to the GraphiQL toolbar 2. Added a GraphiQL footer to display response time and response size for queries 3. Removed 'Copy' and 'Voyager' buttons to make space for 'Cache' button ### Steps to test and verify 1. Click on the 'Cache' button on the GraphiQL toolbar to add @cached directive to your query which enables caching for query response. 2. The response time and response size details will be displayed in the footer below the query response 3. The Cache keyword with a success icon is displayed in the footer when response caching is enabled ### Screenshots 1. Queries without response caching <img width="1440" alt="Screenshot 2021-09-15 at 11 09 23 AM" src="https://user-images.githubusercontent.com/59638722/133379080-9e4fa2ac-2a60-4030-9152-739564fd2419.png"> 2. Response caching enabled <img width="1440" alt="Screenshot 2021-09-15 at 11 09 50 AM" src="https://user-images.githubusercontent.com/59638722/133379089-3ddb321f-ae82-4d57-ab50-0284f151819c.png"> PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2353 Co-authored-by: Rishichandra Wawhal <27274869+wawhal@users.noreply.github.com> GitOrigin-RevId: abc7566085c8ed7e362a0abc2b3906c2c8974cee
This commit is contained in:
parent
7553ca170b
commit
e2e740aa68
@ -46,6 +46,8 @@ const CREATE_WEBSOCKET_CLIENT = 'ApiExplorer/CREATE_WEBSOCKET_CLIENT';
|
||||
const FOCUS_ROLE_HEADER = 'ApiExplorer/FOCUS_ROLE_HEADER';
|
||||
const UNFOCUS_ROLE_HEADER = 'ApiExplorer/UNFOCUS_ROLE_HEADER';
|
||||
|
||||
const TRACK_RESPONSE_DETAILS = 'ApiExplorer/TRACK_RESPONSE_DETAILS';
|
||||
|
||||
let websocketSubscriptionClient;
|
||||
|
||||
const getSubscriptionInstance = (url, headers) => {
|
||||
@ -216,16 +218,30 @@ const isSubscription = graphQlParams => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const graphQLFetcherFinal = (graphQLParams, url, headers, dispatch) => {
|
||||
const graphQLFetcherFinal = (
|
||||
graphQLParams,
|
||||
url,
|
||||
headers,
|
||||
dispatch,
|
||||
requestTrackingId
|
||||
) => {
|
||||
if (isSubscription(graphQLParams)) {
|
||||
return graphqlSubscriber(graphQLParams, url, headers);
|
||||
}
|
||||
return dispatch(
|
||||
requestAction(url, {
|
||||
method: 'POST',
|
||||
headers: getHeadersAsJSON(headers),
|
||||
body: JSON.stringify(graphQLParams),
|
||||
})
|
||||
requestAction(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getHeadersAsJSON([...headers]),
|
||||
body: JSON.stringify(graphQLParams),
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
requestTrackingId
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@ -460,6 +476,18 @@ const getRemoteQueries = (queryUrl, cb, dispatch) => {
|
||||
.catch(e => console.error('Invalid query file URL: ', e));
|
||||
};
|
||||
|
||||
const processResponseDetails = (
|
||||
responseTime,
|
||||
responseSize,
|
||||
isResponseCached,
|
||||
responseTrackingId
|
||||
) => dispatch => {
|
||||
dispatch({
|
||||
type: TRACK_RESPONSE_DETAILS,
|
||||
data: { responseTime, responseSize, isResponseCached, responseTrackingId },
|
||||
});
|
||||
};
|
||||
|
||||
const apiExplorerReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case CHANGE_TAB:
|
||||
@ -675,6 +703,20 @@ const apiExplorerReducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
loading: action.data,
|
||||
};
|
||||
case TRACK_RESPONSE_DETAILS:
|
||||
return {
|
||||
...state,
|
||||
explorerData: {
|
||||
...state.explorerData,
|
||||
response: {
|
||||
...state.explorerData.response,
|
||||
responseTime: action.data.responseTime,
|
||||
responseSize: action.data.responseSize,
|
||||
isResponseCached: action.data.isResponseCached,
|
||||
responseTrackingId: action.data.responseTrackingId,
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -707,4 +749,5 @@ export {
|
||||
analyzeFetcher,
|
||||
verifyJWTToken,
|
||||
setHeadersBulk,
|
||||
processResponseDetails,
|
||||
};
|
||||
|
@ -609,6 +609,7 @@ class ApiRequest extends Component {
|
||||
dispatch={this.props.dispatch}
|
||||
headerFocus={this.props.headerFocus}
|
||||
urlParams={this.props.urlParams}
|
||||
response={this.props.explorerData.response}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -2150,3 +2150,37 @@ li.CodeMirror-hint-active {
|
||||
.CodeMirror-hint-deprecation :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.graphiql_footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: #eee;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.graphiql_footer_label {
|
||||
font-size: 10px;
|
||||
color: #777777;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.graphiql_footer_value {
|
||||
margin-right: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.graphiql_footer_icon {
|
||||
color: #777;
|
||||
margin-right: 8px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.color_green {
|
||||
color: #047857 !important;
|
||||
}
|
||||
|
@ -11,19 +11,18 @@ import {
|
||||
persistCodeExporterOpen,
|
||||
} from '../OneGraphExplorer/utils';
|
||||
|
||||
import {
|
||||
clearCodeMirrorHints,
|
||||
setQueryVariableSectionHeight,
|
||||
copyToClipboard,
|
||||
} from './utils';
|
||||
import { clearCodeMirrorHints, setQueryVariableSectionHeight } from './utils';
|
||||
import { generateRandomString } from '../../../Services/Data/DataSources/CreateDataSource/Heroku/utils';
|
||||
import { analyzeFetcher, graphQLFetcherFinal } from '../Actions';
|
||||
import { parse as sdlParse, print } from 'graphql';
|
||||
import deriveAction from '../../../../shared/utils/deriveAction';
|
||||
import {
|
||||
getActionDefinitionSdl,
|
||||
getTypesSdl,
|
||||
toggleCacheDirective,
|
||||
} from '../../../../shared/utils/sdlUtils';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import ToolTip from '../../../Common/Tooltip/Tooltip';
|
||||
import { getActionsCreateRoute } from '../../../Common/utils/routesUtils';
|
||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
import {
|
||||
@ -33,6 +32,7 @@ import {
|
||||
} from '../../Actions/Add/reducer';
|
||||
import { getGraphQLEndpoint } from '../utils';
|
||||
import snippets from './snippets';
|
||||
import globals from '../../../../Globals';
|
||||
|
||||
import 'graphiql/graphiql.css';
|
||||
import './GraphiQL.css';
|
||||
@ -49,6 +49,7 @@ class GraphiQLWrapper extends Component {
|
||||
onBoardingEnabled: false,
|
||||
copyButtonText: 'Copy',
|
||||
codeExporterOpen: false,
|
||||
requestTrackingId: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,18 +83,29 @@ class GraphiQLWrapper extends Component {
|
||||
mode,
|
||||
loading,
|
||||
} = this.props;
|
||||
const { codeExporterOpen } = this.state;
|
||||
const { codeExporterOpen, requestTrackingId } = this.state;
|
||||
const graphqlNetworkData = this.props.data;
|
||||
const {
|
||||
responseTime,
|
||||
responseSize,
|
||||
isResponseCached,
|
||||
responseTrackingId,
|
||||
} = this.props.response;
|
||||
|
||||
const graphQLFetcher = graphQLParams => {
|
||||
if (headerFocus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trackingId = generateRandomString();
|
||||
this.setState({ requestTrackingId: trackingId });
|
||||
|
||||
return graphQLFetcherFinal(
|
||||
graphQLParams,
|
||||
getGraphQLEndpoint(mode),
|
||||
graphqlNetworkData.headers,
|
||||
dispatch
|
||||
dispatch,
|
||||
trackingId
|
||||
);
|
||||
};
|
||||
|
||||
@ -111,20 +123,6 @@ class GraphiQLWrapper extends Component {
|
||||
editor.setValue(prettyText);
|
||||
};
|
||||
|
||||
const handleCopyQuery = () => {
|
||||
const editor = graphiqlContext.getQueryEditor();
|
||||
const query = editor.getValue();
|
||||
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(query);
|
||||
this.setState({ copyButtonText: 'Copied' });
|
||||
setTimeout(() => {
|
||||
this.setState({ copyButtonText: 'Copy' });
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleToggleHistory = () => {
|
||||
graphiqlContext.setState(prevState => ({
|
||||
historyPaneOpen: !prevState.historyPaneOpen,
|
||||
@ -179,6 +177,42 @@ class GraphiQLWrapper extends Component {
|
||||
dispatch(_push('/api/rest/create'));
|
||||
};
|
||||
|
||||
const _toggleCacheDirective = () => {
|
||||
const editor = graphiqlContext.getQueryEditor();
|
||||
const operationString = editor.getValue();
|
||||
const cacheToggledOperationString = toggleCacheDirective(operationString);
|
||||
editor.setValue(cacheToggledOperationString);
|
||||
};
|
||||
|
||||
const renderGraphiqlFooter = responseTime &&
|
||||
responseTrackingId === requestTrackingId && (
|
||||
<GraphiQL.Footer>
|
||||
<div className="graphiql_footer">
|
||||
<span className="graphiql_footer_label">Response Time</span>
|
||||
<span className="graphiql_footer_value">{responseTime} ms</span>
|
||||
{responseSize && (
|
||||
<>
|
||||
<span className="graphiql_footer_label">Response Size</span>
|
||||
<span className="graphiql_footer_value">
|
||||
{responseSize} bytes
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isResponseCached && (
|
||||
<>
|
||||
<span className="graphiql_footer_label">Cached</span>
|
||||
<ToolTip
|
||||
message="This query response was cached using the @cached directive"
|
||||
placement="top"
|
||||
tooltipStyle="graphiql_footer_icon"
|
||||
/>
|
||||
<i className="fa fa-check-circle color_green" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GraphiQL.Footer>
|
||||
);
|
||||
|
||||
const renderGraphiql = graphiqlProps => {
|
||||
const voyagerUrl = graphqlNetworkData.consoleUrl + '/voyager-view';
|
||||
let analyzerProps = {};
|
||||
@ -199,27 +233,22 @@ class GraphiQLWrapper extends Component {
|
||||
title: 'Show History',
|
||||
onClick: handleToggleHistory,
|
||||
},
|
||||
{
|
||||
label: this.state.copyButtonText,
|
||||
title: 'Copy Query',
|
||||
onClick: handleCopyQuery,
|
||||
},
|
||||
{
|
||||
label: 'Explorer',
|
||||
title: 'Toggle Explorer',
|
||||
onClick: graphiqlProps.toggleExplorer,
|
||||
},
|
||||
{
|
||||
label: 'Cache',
|
||||
title: 'Cache the response of this query',
|
||||
onClick: _toggleCacheDirective,
|
||||
hide: globals.consoleType !== 'cloud',
|
||||
},
|
||||
{
|
||||
label: 'Code Exporter',
|
||||
title: 'Toggle Code Exporter',
|
||||
onClick: this._handleToggleCodeExporter,
|
||||
},
|
||||
{
|
||||
label: 'Voyager',
|
||||
title: 'GraphQL Voyager',
|
||||
onClick: () => window.open(voyagerUrl, '_blank'),
|
||||
icon: <i className="fa fa-external-link" aria-hidden="true" />,
|
||||
},
|
||||
{
|
||||
label: 'REST',
|
||||
title: 'REST Endpoints',
|
||||
@ -233,9 +262,11 @@ class GraphiQLWrapper extends Component {
|
||||
onClick: deriveActionFromOperation,
|
||||
});
|
||||
}
|
||||
return buttons.map(b => {
|
||||
return <GraphiQL.Button key={b.label} {...b} />;
|
||||
});
|
||||
return buttons
|
||||
.filter(b => !b.hide)
|
||||
.map(b => {
|
||||
return <GraphiQL.Button key={b.label} {...b} />;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -258,6 +289,7 @@ class GraphiQLWrapper extends Component {
|
||||
{...analyzerProps}
|
||||
/>
|
||||
</GraphiQL.Toolbar>
|
||||
{renderGraphiqlFooter}
|
||||
</GraphiQL>
|
||||
{codeExporterOpen ? (
|
||||
<CodeExporter
|
||||
@ -301,6 +333,7 @@ GraphiQLWrapper.propTypes = {
|
||||
numberOfTables: PropTypes.number.isRequired,
|
||||
headerFocus: PropTypes.bool.isRequired,
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
response: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -47,7 +47,10 @@ const dataApis = {
|
||||
const explorerData = {
|
||||
sendingRequest: false,
|
||||
enableResponseSection: false,
|
||||
response: {},
|
||||
response: {
|
||||
responseTime: null,
|
||||
responseSize: null,
|
||||
},
|
||||
fileObj: null,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { parse as sdlParse } from 'graphql/language/parser';
|
||||
import { print as sdlPrint } from 'graphql/language/printer';
|
||||
import { getAstTypeMetadata, wrapTypename } from './wrappingTypeUtils';
|
||||
import {
|
||||
reformCustomTypes,
|
||||
@ -367,3 +368,46 @@ export const getSdlComplete = (allActions, allTypes) => {
|
||||
sdl += getTypesSdl(allTypes);
|
||||
return sdl;
|
||||
};
|
||||
|
||||
export const toggleCacheDirective = operationString => {
|
||||
let operationAst;
|
||||
try {
|
||||
operationAst = sdlParse(operationString);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldAddCacheDirective = !operationAst.definitions.some(def => {
|
||||
return def.directives.some(dir => dir.name.value === 'cached');
|
||||
});
|
||||
|
||||
const newOperationAst = JSON.parse(JSON.stringify(operationAst));
|
||||
|
||||
newOperationAst.definitions = operationAst.definitions.map(def => {
|
||||
if (def.kind === 'OperationDefinition' && def.operation === 'query') {
|
||||
const newDef = {
|
||||
...def,
|
||||
directives: def.directives.filter(dir => dir.name.value !== 'cached'),
|
||||
};
|
||||
if (shouldAddCacheDirective) {
|
||||
newDef.directives.push({
|
||||
kind: 'Directive',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'cached',
|
||||
},
|
||||
});
|
||||
}
|
||||
return newDef;
|
||||
}
|
||||
return def;
|
||||
});
|
||||
|
||||
try {
|
||||
const newString = sdlPrint(newOperationAst);
|
||||
return newString;
|
||||
} catch {
|
||||
throw new Error('cannot build the operation string');
|
||||
}
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
CONNECTION_FAILED,
|
||||
} from '../components/App/Actions';
|
||||
import { globalCookiePolicy } from '../Endpoints';
|
||||
import { processResponseDetails } from '../components/Services/ApiExplorer/Actions';
|
||||
|
||||
const requestAction = <T = any>(
|
||||
url: string,
|
||||
@ -18,7 +19,8 @@ const requestAction = <T = any>(
|
||||
SUCCESS?: string,
|
||||
ERROR?: string,
|
||||
includeCredentials = true,
|
||||
includeAdminHeaders = false
|
||||
includeAdminHeaders = false,
|
||||
requestTrackingId?: string
|
||||
): Thunk<Promise<T>> => {
|
||||
return (dispatch: any, getState: any) => {
|
||||
const requestOptions = { ...options };
|
||||
@ -35,10 +37,12 @@ const requestAction = <T = any>(
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch({ type: LOAD_REQUEST });
|
||||
const startTime = new Date().getTime();
|
||||
fetch(url, requestOptions).then(
|
||||
response => {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
const isResponseJson = `${contentType}`.includes('application/json');
|
||||
|
||||
if (response.ok) {
|
||||
if (!isResponseJson) {
|
||||
return response.text().then(responseBody => {
|
||||
@ -56,6 +60,21 @@ const requestAction = <T = any>(
|
||||
dispatch({ type: SUCCESS, data: results });
|
||||
}
|
||||
dispatch({ type: DONE_REQUEST });
|
||||
if (requestTrackingId) {
|
||||
const endTime = new Date().getTime();
|
||||
const responseTimeMs = endTime - startTime;
|
||||
const isResponseCached = response.headers.has('Cache-Control');
|
||||
const responseSize = JSON.stringify(results).length * 2;
|
||||
dispatch(
|
||||
processResponseDetails(
|
||||
responseTimeMs,
|
||||
responseSize,
|
||||
isResponseCached,
|
||||
requestTrackingId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
resolve(results);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user