Skip to content

Commit

Permalink
feat(playground): GraphiQL sandbox. Allow using the cube GraphQL API (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
vasilev-alex committed Dec 14, 2021
1 parent 67ca6c8 commit 6c848c0
Show file tree
Hide file tree
Showing 15 changed files with 2,496 additions and 614 deletions.
64 changes: 55 additions & 9 deletions packages/cubejs-api-gateway/src/graphql.ts
Expand Up @@ -147,8 +147,17 @@ function mapWhereValue(operator: string, value: any) {
switch (operator) {
case 'set':
return undefined;
case 'inDateRange':
case 'notInDateRange':
// This is a hack for named date ranges (e.g. "This year", "Today")
// We should use enums in the future
if (value.length === 1 && !/^[0-9]/.test(value[0])) {
return value[0].toString();
}

return value.map(v => v.toString());
default:
return Array.isArray(value) ? value.map(v => `${v}`) : [`${value}`];
return Array.isArray(value) ? value.map(v => v.toString()) : [value.toString()];
}
}

Expand Down Expand Up @@ -236,7 +245,7 @@ function whereArgToQueryFilters(
prefix?: string
) {
const queryFilters: any[] = [];

Object.keys(whereArg).forEach((key) => {
if (['OR', 'AND'].includes(key)) {
queryFilters.push({
Expand Down Expand Up @@ -412,6 +421,17 @@ export function makeSchema(metaConfig: any) {
});
}
}));

types.push(inputObjectType({
name: 'RootOrderByInput',
definition(t) {
metaConfig.forEach(cube => {
t.field(unCapitalize(cube.config.name), {
type: `${cube.config.name}OrderByInput`
});
});
}
}));

types.push(objectType({
name: 'Result',
Expand Down Expand Up @@ -445,49 +465,75 @@ export function makeSchema(metaConfig: any) {
offset: intArg(),
timezone: stringArg(),
renewQuery: booleanArg(),
orderBy: arg({
type: 'RootOrderByInput'
}),
},
resolve: async (_, { where, limit, offset, timezone, renewQuery }, { req, apiGateway }, infos) => {
resolve: async (_, { where, limit, offset, timezone, orderBy, renewQuery }, { req, apiGateway }, infos) => {
const measures: string[] = [];
const dimensions: string[] = [];
const timeDimensions: any[] = [];
let filters: any[] = [];
const order: Record<string, string> = {};
const order: [string, 'asc' | 'desc'][] = [];

if (where) {
filters = whereArgToQueryFilters(where);
}

if (orderBy) {
Object.entries<any>(orderBy).forEach(([cubeName, members]) => {
Object.entries<any>(members).forEach(([member, value]) => {
order.push([`${capitalize(cubeName)}.${member}`, value]);
});
});
}

getFieldNodeChildren(infos.fieldNodes[0], infos).forEach(cubeNode => {
const cubeName = capitalize(cubeNode.name.value);
const orderByArg = getArgumentValue(cubeNode, 'orderBy');
// todo: throw if both RootOrderByInput and [Cube]OrderByInput provided
if (orderByArg) {
Object.keys(orderByArg).forEach(key => {
order[`${cubeName}.${key}`] = orderByArg[key];
order.push([`${cubeName}.${key}`, orderByArg[key]]);
});
}

const whereArg = getArgumentValue(cubeNode, 'where');
if (whereArg) {
filters = whereArgToQueryFilters(whereArg, cubeName).concat(filters);
}

const inDateRangeFilters = {};
filters = filters.filter((f) => {
if (f.operator === 'inDateRange') {
inDateRangeFilters[f.member] = f.values;
return false;
}

return true;
});

getFieldNodeChildren(cubeNode, infos).forEach(memberNode => {
const memberName = memberNode.name.value;
const memberType = getMemberType(metaConfig, cubeName, memberName);
const key = `${cubeName}.${memberName}`;

if (memberType === 'measure') {
measures.push(`${cubeName}.${memberName}`);
measures.push(key);
} else if (memberType === 'dimension') {
const granularityNodes = getFieldNodeChildren(memberNode, infos);
if (granularityNodes.length > 0) {
granularityNodes.forEach(granularityNode => {
const granularityName = granularityNode.name.value;
if (granularityName === 'value') {
dimensions.push(`${cubeName}.${memberName}`);
dimensions.push(key);
} else {
timeDimensions.push({
dimension: `${cubeName}.${memberName}`,
granularity: granularityName
dimension: key,
granularity: granularityName,
...(inDateRangeFilters[key] ? {
dateRange: inDateRangeFilters[key],
} : null)
});
}
});
Expand Down
4 changes: 2 additions & 2 deletions packages/cubejs-api-gateway/tsconfig.json
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"src",
"test"
"src/**/*",
"test/**/*"
],
"compilerOptions": {
"outDir": "dist",
Expand Down
1 change: 1 addition & 0 deletions packages/cubejs-playground/.env
@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true
11 changes: 8 additions & 3 deletions packages/cubejs-playground/package.json
Expand Up @@ -11,6 +11,7 @@
"module": "lib/index.js",
"types": "lib/playground/index.d.ts",
"scripts": {
"unit": "react-scripts test",
"dev": "kill-port 3080 && yarn start",
"start": "SKIP_PREFLIGHT_CHECK=true PORT=3080 react-app-rewired start",
"watch": "rollup -c -w",
Expand All @@ -35,20 +36,23 @@
"@ant-design/icons": "^4.1.0",
"@cubejs-client/core": "^0.28.52",
"@cubejs-client/react": "^0.28.52",
"@graphiql/toolkit": "^0.4.2",
"camel-case": "^4.1.2",
"codesandbox-import-utils": "^2.1.1",
"cron-validator": "^1.2.1",
"customize-cra": "^1.0.0",
"fast-deep-equal": "^3.1.3",
"fetch-retry": "^4.0.1",
"flexsearch": "^0.6.32",
"graphiql": "^1.5.15",
"history": "^4.9.0",
"js-cookie": "^2.2.1",
"js-object-pretty-print": "^0.3.0",
"jwt-decode": "^3.1.2",
"less": "^4.1.1",
"mitt": "^2.1.0",
"moment": "^2.24.0",
"prettier": "^2.5.1",
"prismjs": "^1.23.0",
"react-beautiful-dnd": "^13.0.0",
"react-chartjs-2": "^2.7.4",
Expand Down Expand Up @@ -85,7 +89,6 @@
"eslint-plugin-react": "^7.20.0",
"fs-extra": "^8.1.0",
"less-loader": "^8.1.1",
"prettier": "^2.3.2",
"react": "^17.0.1",
"react-app-rewire-yarn-workspaces": "^1.0.3",
"react-app-rewired": "^2.1.0",
Expand All @@ -97,12 +100,14 @@
"rollup-plugin-svg": "^2.0.0",
"styled-components": "5.2.0",
"tslib": "^2.3.0",
"typescript": "^4.3.5"
"typescript": "^4.3.5",
"graphql": "^15.3.0"
},
"peerDependencies": {
"antd": ">=4.13.0",
"react": ">=17.0.1",
"react-dom": ">=17.0.1",
"styled-components": ">=5.2.0"
"styled-components": ">=5.2.0",
"graphql": ">=15.3.0"
}
}
70 changes: 54 additions & 16 deletions packages/cubejs-playground/src/ChartContainer.tsx
@@ -1,4 +1,4 @@
import { Component, useEffect, FunctionComponent } from 'react';
import { Component, useEffect, FunctionComponent, lazy, Suspense } from 'react';
import {
CodeOutlined,
CodeSandboxOutlined,
Expand All @@ -13,16 +13,21 @@ import { getParameters } from 'codesandbox-import-utils/lib/api/define';
import styled from 'styled-components';
import { Redirect, RouteComponentProps, withRouter } from 'react-router-dom';
import { QueryRenderer } from '@cubejs-client/react';
import { ChartType, Query, ResultSet } from '@cubejs-client/core';
import { ChartType, Meta, Query, ResultSet } from '@cubejs-client/core';
import { format } from 'sql-formatter';

import { SectionRow } from './components';
import { Button, Card, FatalError } from './atoms';
import { Button, Card, CubeLoader, FatalError } from './atoms';
import PrismCode from './PrismCode';
import CachePane from './components/CachePane';
import { playgroundAction } from './events';
import { codeSandboxDefinition, copyToClipboard } from './utils';
import DashboardSource from './DashboardSource';
import { GraphQLIcon } from './shared/icons/GraphQLIcon';

const GraphiQLSandbox = lazy(
() => import('./components/GraphQL/GraphiQLSandbox')
);

const frameworkToTemplate = {
react: 'create-react-app',
Expand All @@ -31,6 +36,8 @@ const frameworkToTemplate = {
};

const StyledCard: any = styled(Card)`
min-height: 420px;
.ant-card-head {
position: sticky;
top: 0;
Expand All @@ -54,11 +61,11 @@ type FrameworkDescriptor = {
scaffoldingSupported?: boolean;
};

const UnsupportedFrameworkPlaceholder: UnsupportedPlaceholder = ({ framework }) => (
const UnsupportedFrameworkPlaceholder: UnsupportedPlaceholder = ({
framework,
}) => (
<h2 style={{ padding: 24, textAlign: 'center' }}>
We do not support&nbsp;
Vanilla JavaScript
&nbsp;code generation here yet.
We do not support&nbsp; Vanilla JavaScript &nbsp;code generation here yet.
<br />
Please refer to&nbsp;
<a
Expand All @@ -69,28 +76,25 @@ const UnsupportedFrameworkPlaceholder: UnsupportedPlaceholder = ({ framework })
playgroundAction('Unsupported Framework Docs', { framework })
}
>
Vanilla JavaScript
&nbsp;docs
Vanilla JavaScript &nbsp;docs
</a>
&nbsp;to see on how to use it with Cube.js.
</h2>
);

const BIPlaceholder: UnsupportedPlaceholder = () => (
<h2 style={{ padding: 24, textAlign: 'center' }}>
You can connect Cube to any Business Intelligence tool through the Cube SQL API.
You can connect Cube to any Business Intelligence tool through the Cube SQL
API.
<br />
Please refer to&nbsp;
<a
href="https://cube.dev/docs/backend/sql"
target="_blank"
rel="noopener noreferrer"
onClick={() =>
playgroundAction('BI Docs' )
}
onClick={() => playgroundAction('BI Docs')}
>
Cube SQL
&nbsp;docs
Cube SQL &nbsp;docs
</a>
&nbsp;to learn more.
</h2>
Expand Down Expand Up @@ -126,8 +130,10 @@ export const frameworks: FrameworkDescriptor[] = [

type ChartContainerProps = {
query: Query;
meta: Meta;
hideActions: boolean;
chartType: ChartType;
isGraphQLSupported: boolean;
dashboardSource?: DashboardSource;
error?: Error;
resultSet?: ResultSet;
Expand Down Expand Up @@ -237,6 +243,7 @@ class ChartContainer extends Component<
history,
framework,
setFramework,
meta,
isFetchingMeta,
onChartRendererReadyChange,
} = this.props;
Expand Down Expand Up @@ -361,6 +368,20 @@ class ChartContainer extends Component<
JSON Query
</Button>

<Button
data-testid="graphiql-btn"
icon={<GraphQLIcon />}
size="small"
type={showCode === 'graphiql' ? 'primary' : 'default'}
disabled={!!frameworkItem?.placeholder || isFetchingMeta}
onClick={() => {
playgroundAction('Show GraphiQL');
this.setState({ showCode: 'graphiql' });
}}
>
GraphiQL
</Button>

<Button
data-testid="code-btn"
icon={<CodeOutlined />}
Expand Down Expand Up @@ -516,7 +537,22 @@ class ChartContainer extends Component<
);
} else if (showCode === 'cache') {
return <CachePane query={query} />;
} else if (showCode === 'graphiql' && meta) {
if (!this.props.isGraphQLSupported) {
return <div>GraphQL API is supported since version 0.28.56</div>
}

return (
<Suspense fallback={<div style={{ height: 363 }}><CubeLoader /></div>}>
<GraphiQLSandbox
apiUrl={this.props.apiUrl}
query={query}
meta={meta}
/>
</Suspense>
);
}

return render({ framework, error });
};

Expand Down Expand Up @@ -584,14 +620,16 @@ class ChartContainer extends Component<
);
} else if (showCode === 'cache') {
title = 'Cache';
} else if (showCode === 'graphiql') {
title = 'GraphQL API';
} else {
title = 'Chart';
}

return hideActions ? (
render({ resultSet, error })
) : (
<StyledCard title={title} style={{ minHeight: 420 }} extra={extra}>
<StyledCard title={title} extra={extra}>
{renderChart()}
</StyledCard>
);
Expand Down

0 comments on commit 6c848c0

Please sign in to comment.