Skip to content

Commit ec23b28

Browse files
authoredFeb 16, 2022
Console: GraphQL Explorer (sst#1374)
1 parent adec9f4 commit ec23b28

File tree

19 files changed

+1799
-79
lines changed

19 files changed

+1799
-79
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ SST also comes with [a set of serverless specific higher-level CDK constructs][r
8181
- [Script](https://docs.serverless-stack.com/constructs/Script) for running scripts while deploying
8282
- [KinesisStream](https://docs.serverless-stack.com/constructs/KinesisStream) for real-time data streaming
8383
- [WebSocketApi](https://docs.serverless-stack.com/constructs/WebSocketApi) for creating WebSocket APIs
84-
- [ApolloApi](https://docs.serverless-stack.com/constructs/ApolloApi) for using Apollo Server with Lambda
84+
- [GraphQLApi](https://docs.serverless-stack.com/constructs/GraphQLApi) for using GraphQL with Lambda
8585
- [EventBus](https://docs.serverless-stack.com/constructs/EventBus) for creating EventBridge Event buses
8686
- [AppSyncApi](https://docs.serverless-stack.com/constructs/AppSyncApi) for creating AppSync GraphQL APIs
8787
- [ApiGatewayV1Api](https://docs.serverless-stack.com/constructs/ApiGatewayV1Api) for using AWS API Gateway v1

‎examples/graphql-apollo/stacks/MyStack.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export default class MyStack extends sst.Stack {
44
constructor(scope: sst.App, id: string, props?: sst.StackProps) {
55
super(scope, id, props);
66

7-
// Create the Apollo GraphQL API
8-
const api = new sst.ApolloApi(this, "ApolloApi", {
7+
// Create the GraphQL API
8+
const api = new sst.GraphQLApi(this, "ApolloApi", {
99
server: "src/lambda.handler",
1010
});
1111

‎packages/cli/test/playground/lib/apollo-api-stack.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export class MainStack extends sst.Stack {
44
constructor(scope: sst.App, id: string, props?: sst.StackProps) {
55
super(scope, id, props);
66

7-
const api = new sst.ApolloApi(this, "Api", {
7+
const api = new sst.GraphQLApi(this, "Api", {
88
server: "src/apollo/graphql.handler",
99
});
1010

‎packages/console/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"buffer": "^6.0.3",
3636
"dendriform-immer-patch-optimiser": "^2.1.0",
3737
"file-saver": "^2.0.5",
38+
"graphql-playground-react": "1.7.28",
3839
"jotai": "^1.4.7",
3940
"react": "^17.0.2",
4041
"react-ace": "^9.5.0",
@@ -44,6 +45,7 @@
4445
"react-icons": "^4.3.1",
4546
"react-json-view": "^1.21.3",
4647
"react-query": "^3.34.2",
48+
"react-redux": "7.2.6",
4749
"react-router-dom": "^6.1.1",
4850
"react-spinners": "^0.11.0",
4951
"react-virtual": "^2.10.0",

‎packages/console/public/graphql.html

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset=utf-8/>
6+
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
7+
<title>GraphQL Playground</title>
8+
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
9+
<link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
10+
<link rel="stylesheet" href="/@fs/home/thdxr/dev/projects/sst/serverless-stack/node_modules/@fontsource/jetbrains-mono/latin.css" />
11+
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
12+
</head>
13+
14+
<body>
15+
<style>
16+
.lbSfdq {
17+
opacity: 1;
18+
transform: initial;
19+
animation: initial;
20+
}
21+
.hlyxcp {
22+
background: transparent;
23+
}
24+
.emFIyf {
25+
display: none;
26+
}
27+
.sc-daURTG {
28+
height: 41px;
29+
}
30+
.dDuAEY {
31+
margin-top: 0;
32+
}
33+
.sc-cmthru {
34+
margin-right: 0;
35+
}
36+
.jsBJIl {
37+
height: calc(-41px + 100vh);
38+
}
39+
</style>
40+
<div id="root">
41+
</div>
42+
<script>
43+
const { searchParams } = new URL(document.location)
44+
const config = JSON.parse(atob(searchParams.get('config')))
45+
console.log(config)
46+
window.addEventListener('load', function (event) {
47+
GraphQLPlayground.init(document.getElementById('root'), config)
48+
})</script>
49+
</body>
50+
51+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Navigate, Route, Routes, useParams } from "react-router-dom";
2+
import { useConstruct, useStacks } from "~/data/aws/stacks";
3+
import { useAuth, useDarkMode } from "~/data/global";
4+
import { styled } from "~/stitches.config";
5+
import { GraphQLApiMetadata } from "../../../../../resources/src/Metadata";
6+
import {
7+
Header,
8+
HeaderTitle,
9+
HeaderSwitcher,
10+
HeaderSwitcherItem,
11+
HeaderSwitcherLabel,
12+
HeaderGroup,
13+
HeaderSwitcherGroup,
14+
} from "../components";
15+
16+
export function GraphQL() {
17+
return (
18+
<Routes>
19+
<Route path=":stack/:addr/*" element={<Explorer />} />
20+
<Route path="*" element={<Explorer />} />
21+
</Routes>
22+
);
23+
}
24+
25+
const Root = styled("div", {
26+
display: "flex",
27+
height: "100%",
28+
width: "100%",
29+
overflow: "hidden",
30+
});
31+
32+
const Main = styled("div", {
33+
display: "flex",
34+
flexDirection: "column",
35+
flexGrow: 1,
36+
height: "100%",
37+
overflow: "hidden",
38+
});
39+
40+
const Content = styled("div", {
41+
overflow: "hidden",
42+
flexGrow: 1,
43+
"& iframe": {
44+
width: "100%",
45+
height: "100%",
46+
},
47+
});
48+
49+
const Empty = styled("div", {
50+
padding: "$lg",
51+
});
52+
53+
export function Explorer() {
54+
const stacks = useStacks();
55+
const params = useParams<{ stack: string; addr: string; "*": string }>();
56+
const apis = (stacks.data?.constructs.byType["Api"] || []).filter(
57+
(item): item is GraphQLApiMetadata => item.data.graphql
58+
);
59+
const appsync = stacks.data?.constructs.byType["AppSync"] || [];
60+
const constructs = [...apis, ...appsync];
61+
const selected = useConstruct("Api", params.stack!, params.addr!);
62+
const dm = useDarkMode();
63+
64+
if (constructs.length > 0 && !selected)
65+
return <Navigate to={`${constructs[0].stack}/${constructs[0].addr}`} />;
66+
67+
return (
68+
<Root>
69+
<Main>
70+
<Header>
71+
<HeaderTitle>GraphQL</HeaderTitle>
72+
{constructs.length > 0 && (
73+
<HeaderGroup>
74+
<HeaderSwitcher value={`${selected.stack} / ${selected.id}`}>
75+
{stacks.data?.all
76+
.filter(
77+
(s) =>
78+
(s.constructs.byType["AppSync"]?.length || 0) +
79+
(s.constructs.byType.Api?.filter((x) => x.data.graphql)
80+
.length || 0) >
81+
0
82+
)
83+
.map((stack) => (
84+
<HeaderSwitcherGroup>
85+
<HeaderSwitcherLabel>
86+
{stack.info.StackName}
87+
</HeaderSwitcherLabel>
88+
{stack.constructs.byType.Api?.map((item) => (
89+
<HeaderSwitcherItem
90+
key={item.stack + item.addr}
91+
to={`../${item.stack}/${item.addr}`}
92+
>
93+
{item.id}
94+
</HeaderSwitcherItem>
95+
))}
96+
{stack.constructs.byType.AppSync?.map((item) => (
97+
<HeaderSwitcherItem
98+
key={item.stack + item.addr}
99+
to={`../${item.stack}/${item.addr}`}
100+
>
101+
{item.id}
102+
</HeaderSwitcherItem>
103+
))}
104+
</HeaderSwitcherGroup>
105+
))}
106+
</HeaderSwitcher>
107+
</HeaderGroup>
108+
)}
109+
</Header>
110+
<Content>
111+
{selected && (
112+
<iframe
113+
src={`/graphql.html?config=${btoa(
114+
JSON.stringify({
115+
endpoint: selected.data.url,
116+
settings: {
117+
"editor.fontFamily": "'Jetbrains Mono'",
118+
"editor.theme": dm.enabled ? "dark" : "light",
119+
"schema.polling.endpointFilter": "*",
120+
},
121+
})
122+
)}`}
123+
/>
124+
)}
125+
{constructs.length === 0 && (
126+
<Empty>No GraphQL APIs in this app</Empty>
127+
)}
128+
</Content>
129+
</Main>
130+
</Root>
131+
);
132+
}

‎packages/console/src/App/Stage/Panel.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BsMoon,
99
} from "react-icons/bs";
1010
import { FaDatabase } from "react-icons/fa";
11+
import { GrGraphQl } from "react-icons/gr";
1112
import { Favicon, Logo, Stack } from "~/components";
1213
import { styled } from "~/stitches.config";
1314
import { Link, NavLink, useParams } from "react-router-dom";
@@ -185,6 +186,10 @@ export function Panel() {
185186
<FaDatabase />
186187
<MenuLabel>RDS</MenuLabel>
187188
</MenuItem>
189+
<MenuItem to="graphql">
190+
<GrGraphQl />
191+
<MenuLabel>GraphQL</MenuLabel>
192+
</MenuItem>
188193
</Menu>
189194
{!expand && (
190195
<DarkMode onClick={dm.toggle}>

‎packages/console/src/App/Stage/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useRealtimeState } from "~/data/global";
1212
import { trpc } from "~/data/trpc";
1313
import { useEffect, useRef } from "react";
1414
import { RDS } from "./RDS";
15+
import { GraphQL } from "./GraphQL";
1516

1617
const Root = styled("div", {
1718
background: "$loContrast",
@@ -56,6 +57,7 @@ export function Stage() {
5657
<Route path="cognito/*" element={<Cognito />} />
5758
<Route path="buckets/*" element={<Buckets />} />
5859
<Route path="rds/*" element={<RDS />} />
60+
<Route path="graphql/*" element={<GraphQL />} />
5961
</Routes>
6062
</Content>
6163
</Fill>

‎packages/console/src/data/aws/api.ts

Whitespace-only changes.

‎packages/resources/src/Api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ export class Api extends Construct implements SSTConstruct {
334334
return {
335335
type: "Api" as const,
336336
data: {
337+
graphql: false,
338+
url: this.httpApi.url,
337339
httpApiId: this.httpApi.apiId,
338340
customDomainUrl: this._customDomainUrl,
339341
routes: Object.entries(this.routesData).map(([key, data]) => {

‎packages/resources/src/AppSyncApi.ts

+1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class AppSyncApi extends Construct implements SSTConstruct {
215215
return {
216216
type: "AppSync" as const,
217217
data: {
218+
url: this.graphqlApi.graphqlUrl,
218219
appSyncApiId: this.graphqlApi.apiId,
219220
dataSources: Object.entries(this.dataSourcesByDsKey).map(([key]) => ({
220221
name: key,

0 commit comments

Comments
 (0)
Please sign in to comment.