@bestcodetools/graphql-playground 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +8 -0
- package/.github/workflows/docker-publish.yml +48 -0
- package/.github/workflows/npm-publish.yml +22 -0
- package/Dockerfile +28 -0
- package/README.md +110 -0
- package/jest.config.js +8 -0
- package/package.json +49 -0
- package/public/components/config-editor.html +38 -0
- package/public/components/field-args.html +16 -0
- package/public/components/field-type.html +27 -0
- package/public/components/headers-editor.html +3 -0
- package/public/components/query-editor.html +3 -0
- package/public/components/response-viewer.html +3 -0
- package/public/components/schema-viewer.html +74 -0
- package/public/components/variables-editor.html +3 -0
- package/public/index.html +114 -0
- package/public/js/components/config-editor.js +122 -0
- package/public/js/components/headers-editor.js +562 -0
- package/public/js/components/query-editor.js +1896 -0
- package/public/js/components/response-viewer.js +201 -0
- package/public/js/components/schema-viewer.js +644 -0
- package/public/js/components/variables-editor.js +1258 -0
- package/public/js/main.js +1016 -0
- package/public/styles/main.css +1313 -0
- package/public/styles/main.css.map +1 -0
- package/public/styles/main.scss +1319 -0
- package/src/middleware.ts +36 -0
- package/src/standalone.ts +14 -0
- package/tests/standalone.test.ts +74 -0
- package/tsconfig.json +71 -0
package/.dockerignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Docker Image
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
docker:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up QEMU
|
|
20
|
+
uses: docker/setup-qemu-action@v3
|
|
21
|
+
|
|
22
|
+
- name: Set up Docker Buildx
|
|
23
|
+
uses: docker/setup-buildx-action@v3
|
|
24
|
+
|
|
25
|
+
- name: Log in to Docker Hub
|
|
26
|
+
uses: docker/login-action@v3
|
|
27
|
+
with:
|
|
28
|
+
registry: docker.io
|
|
29
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
30
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
31
|
+
|
|
32
|
+
- name: Extract Docker metadata
|
|
33
|
+
id: meta
|
|
34
|
+
uses: docker/metadata-action@v5
|
|
35
|
+
with:
|
|
36
|
+
images: douglasdomingues/graphql-playground
|
|
37
|
+
tags: |
|
|
38
|
+
type=raw,value=latest
|
|
39
|
+
type=ref,event=tag
|
|
40
|
+
|
|
41
|
+
- name: Build and push multi-arch image
|
|
42
|
+
uses: docker/build-push-action@v6
|
|
43
|
+
with:
|
|
44
|
+
context: .
|
|
45
|
+
push: true
|
|
46
|
+
platforms: linux/amd64,linux/arm64
|
|
47
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
48
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish Package to npmjs
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: '20.x'
|
|
16
|
+
registry-url: 'https://registry.npmjs.org'
|
|
17
|
+
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm publish --provenance --access public
|
|
20
|
+
env:
|
|
21
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
22
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
FROM node:22-bookworm-slim AS build
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
|
|
4
|
+
COPY package*.json ./
|
|
5
|
+
RUN npm install
|
|
6
|
+
|
|
7
|
+
COPY tsconfig.json ./
|
|
8
|
+
COPY src ./src
|
|
9
|
+
COPY public ./public
|
|
10
|
+
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
FROM node:22-bookworm-slim AS runtime
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
|
|
16
|
+
ENV NODE_ENV=production
|
|
17
|
+
ENV PLAYGROUND_PORT=3000
|
|
18
|
+
ENV PLAYGROUND_LIVE_RELOAD=false
|
|
19
|
+
|
|
20
|
+
COPY package*.json ./
|
|
21
|
+
RUN npm install --omit=dev
|
|
22
|
+
|
|
23
|
+
COPY --from=build /app/dist ./dist
|
|
24
|
+
COPY --from=build /app/public ./public
|
|
25
|
+
|
|
26
|
+
EXPOSE 3000
|
|
27
|
+
|
|
28
|
+
CMD ["node", "dist/standalone.js"]
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# GraphQL Playground
|
|
2
|
+
|
|
3
|
+
A standalone GraphQL Playground package with a custom dark UI, schema explorer, smart editors, workspace import/export, and Docker support.
|
|
4
|
+
|
|
5
|
+
## What This Package Is For
|
|
6
|
+
|
|
7
|
+
This package serves a browser-based GraphQL playground that helps developers:
|
|
8
|
+
|
|
9
|
+
- write and run GraphQL queries, mutations, and subscriptions
|
|
10
|
+
- inspect the schema in a side panel
|
|
11
|
+
- edit variables and headers with guided autocomplete
|
|
12
|
+
- view formatted JSON responses with syntax highlighting
|
|
13
|
+
- save, import, and export full workspaces with multiple tabs
|
|
14
|
+
|
|
15
|
+
It is designed to be embedded into an Express application or run as a standalone local server.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- standalone Express server for quick local usage
|
|
20
|
+
- custom schema viewer with search and hover details
|
|
21
|
+
- query editor with GraphQL autocomplete and inline tooltips
|
|
22
|
+
- variables editor with schema-aware suggestions
|
|
23
|
+
- headers editor with common header suggestions
|
|
24
|
+
- response viewer with JSON syntax highlighting
|
|
25
|
+
- multi-tab workspace with import/export support
|
|
26
|
+
- Docker image support for a compiled runtime
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @bestcodetools/graphql-playground
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Standalone Usage
|
|
35
|
+
|
|
36
|
+
For development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run standalone
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The playground will be available at:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
http://localhost:3000/playground
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can change the port with:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
PLAYGROUND_PORT=4000 npm run standalone
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Compiled Runtime
|
|
55
|
+
|
|
56
|
+
To run the compiled standalone server without `ts-node-dev`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run build
|
|
60
|
+
npm start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Docker Usage
|
|
64
|
+
|
|
65
|
+
Build the image:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
docker build -t graphql-playground .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Run the container:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
docker run -p 3000:3000 graphql-playground
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then open:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
http://localhost:3000/playground
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Available Scripts
|
|
84
|
+
|
|
85
|
+
- `npm run standalone`: starts the standalone server with `ts-node-dev`
|
|
86
|
+
- `npm run build`: transpiles TypeScript into `dist`
|
|
87
|
+
- `npm start`: runs the transpiled standalone server
|
|
88
|
+
- `npm test`: runs the Jest test suite
|
|
89
|
+
- `npm run transpile:sass:watch`: watches and transpiles Sass files
|
|
90
|
+
|
|
91
|
+
## Testing
|
|
92
|
+
|
|
93
|
+
This package includes a basic integration test for the standalone server.
|
|
94
|
+
|
|
95
|
+
Run:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm test
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The test verifies that the standalone server starts on an automatically assigned port and responds with `200` on the configured playground path.
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- The standalone runtime disables live reload in production mode.
|
|
106
|
+
- Workspace export sanitizes sensitive header values such as authorization, token, and key headers by replacing them with placeholders.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
ISC
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bestcodetools/graphql-playground",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A standalone GraphQL Playground package with a custom dark UI, schema explorer, smart editors, workspace import/export, and Docker support.",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"homepage": "https://github.com/BestCodeTools/graphql-playground#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/BestCodeTools/graphql-playground/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/BestCodeTools/graphql-playground.git"
|
|
13
|
+
},
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"author": "BestCodeTools",
|
|
16
|
+
"type": "commonjs",
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"directories": {
|
|
19
|
+
"test": "tests"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --outDir dist",
|
|
23
|
+
"start": "node dist/standalone.js",
|
|
24
|
+
"standalone": "ts-node-dev --transpile-only src/standalone.ts",
|
|
25
|
+
"transpile:sass:watch": "sass --watch public/styles:public/styles",
|
|
26
|
+
"test": "jest"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"express": "^4.21.2"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/connect-livereload": "^0.6.3",
|
|
33
|
+
"@types/express": "^5.0.0",
|
|
34
|
+
"@types/jest": "^29.5.14",
|
|
35
|
+
"@types/livereload": "^0.9.5",
|
|
36
|
+
"@types/node": "^22.13.4",
|
|
37
|
+
"connect-livereload": "^0.6.1",
|
|
38
|
+
"jest": "^29.7.0",
|
|
39
|
+
"livereload": "^0.9.3",
|
|
40
|
+
"sass": "^1.85.0",
|
|
41
|
+
"ts-jest": "^29.2.5",
|
|
42
|
+
"ts-node": "^10.9.2",
|
|
43
|
+
"ts-node-dev": "^2.0.0",
|
|
44
|
+
"typescript": "^5.7.3"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="config-tabs">
|
|
2
|
+
<div class="tab-item" ng-repeat="tab in $ctrl.tabs track by tab.id"
|
|
3
|
+
ng-class="{ 'active': $ctrl.activeTab === tab.id }"
|
|
4
|
+
ng-click="$ctrl.setActiveTab(tab.id)">
|
|
5
|
+
{{ $ctrl.t(tab.titleKey) }}
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="config-content">
|
|
9
|
+
<div ng-if="$ctrl.activeTab === 'headers'">
|
|
10
|
+
<h3>{{ $ctrl.t('config.shared_headers') }}</h3>
|
|
11
|
+
<table class="headers-table">
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th class="property-name">{{ $ctrl.t('config.property') }}</th>
|
|
15
|
+
<th class="property-value">{{ $ctrl.t('config.value') }}</th>
|
|
16
|
+
<th class="remove-action"></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<tr ng-repeat="header in $ctrl.headers">
|
|
21
|
+
<td class="property-name"><input placeholder="{{ $ctrl.t('config.key_placeholder') }}" type="text" ng-model="header.key"></td>
|
|
22
|
+
<td class="property-value"><input placeholder="{{ $ctrl.t('config.value_placeholder') }}" type="text" ng-model="header.value"></td>
|
|
23
|
+
<td class="remove-action"><button ng-click="$ctrl.removeHeader($index)">🗑️</button></td>
|
|
24
|
+
</tr>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
27
|
+
<button ng-click="$ctrl.addHeader()">{{ $ctrl.t('config.add_header') }}</button>
|
|
28
|
+
</div>
|
|
29
|
+
<div ng-if="$ctrl.activeTab === 'other'">
|
|
30
|
+
<h3>{{ $ctrl.t('config.other') }}</h3>
|
|
31
|
+
<p class="workspace-actions-note">{{ $ctrl.getWorkspaceLabel('note') }}</p>
|
|
32
|
+
<div class="workspace-actions">
|
|
33
|
+
<button type="button" ng-click="$ctrl.handleExportWorkspace()">{{ $ctrl.getWorkspaceLabel('export') }}</button>
|
|
34
|
+
<button type="button" ng-click="$ctrl.triggerImportWorkspace()">{{ $ctrl.getWorkspaceLabel('import') }}</button>
|
|
35
|
+
<input id="workspace-import-input" class="workspace-import-input" type="file" accept=".json,application/json" onchange="angular.element(this).scope().$ctrl.handleImportWorkspaceFile(event)">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="field-args" ng-if="$ctrl.hasArgs()">
|
|
2
|
+
<span class="field-args-paren">(</span>
|
|
3
|
+
<span class="field-arg" ng-repeat="arg in $ctrl.fieldArgs track by arg.name || $index">
|
|
4
|
+
<span class="field-arg-name" ng-mouseover="$ctrl.showArgTooltip($event, arg)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">{{ arg.name }}</span>
|
|
5
|
+
<span class="colon">:</span>
|
|
6
|
+
<field-type field-ref="arg" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
|
|
7
|
+
<span class="field-arg-default" ng-if="arg.defaultValue">
|
|
8
|
+
<span class="equal">=</span> {{ arg.defaultValue }}
|
|
9
|
+
</span>
|
|
10
|
+
<span class="field-arg-description" ng-if="arg.description" ng-mouseover="$ctrl.showArgTooltip($event, arg)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">
|
|
11
|
+
{{ arg.description }}
|
|
12
|
+
</span>
|
|
13
|
+
<span class="field-arg-separator" ng-if="!$last">, </span>
|
|
14
|
+
</span>
|
|
15
|
+
<span class="field-args-paren">)</span>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<span class="field-type-wrapper" ng-if="$ctrl.getTypeRef()">
|
|
2
|
+
<span ng-if="$ctrl.isListType($ctrl.getTypeRef())">
|
|
3
|
+
<span class="field-type-prefix">[</span>
|
|
4
|
+
<field-type field-ref="$ctrl.getTypeRef().ofType" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.forwardHideTooltip()"></field-type>
|
|
5
|
+
<span class="field-type-suffix">]</span>
|
|
6
|
+
</span>
|
|
7
|
+
<span ng-if="$ctrl.isNonNullType($ctrl.getTypeRef())">
|
|
8
|
+
<field-type field-ref="$ctrl.getTypeRef().ofType" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.forwardHideTooltip()"></field-type>
|
|
9
|
+
<span class="field-type-non-null">!</span>
|
|
10
|
+
</span>
|
|
11
|
+
<span ng-if="!$ctrl.isListType($ctrl.getTypeRef()) && !$ctrl.isNonNullType($ctrl.getTypeRef()) && $ctrl.hasNavigableType($ctrl.getTypeRef())">
|
|
12
|
+
<a
|
|
13
|
+
class="field-type-link"
|
|
14
|
+
href="#{{ $ctrl.getTypeAnchor($ctrl.getTypeRef()) }}"
|
|
15
|
+
ng-click="$ctrl.onTypeAnchorClick($event)"
|
|
16
|
+
ng-mouseover="$ctrl.showTooltip($event)"
|
|
17
|
+
ng-mouseout="$ctrl.hideTooltip()"
|
|
18
|
+
on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)"
|
|
19
|
+
on-hide-tooltip="$ctrl.hideTooltip()"
|
|
20
|
+
>
|
|
21
|
+
<span class="field-type">{{ $ctrl.getTypeLabel($ctrl.getTypeRef()) }}</span>
|
|
22
|
+
</a>
|
|
23
|
+
</span>
|
|
24
|
+
<span class="field-type" ng-if="!$ctrl.isListType($ctrl.getTypeRef()) && !$ctrl.isNonNullType($ctrl.getTypeRef()) && !$ctrl.hasNavigableType($ctrl.getTypeRef())" ng-mouseover="$ctrl.showTooltip($event)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">
|
|
25
|
+
{{ $ctrl.getTypeLabel($ctrl.getTypeRef()) }}
|
|
26
|
+
</span>
|
|
27
|
+
</span>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="schema-viewer-wrapper" ng-class="{ 'open': $ctrl.open }">
|
|
2
|
+
<button class="btn btn-toggle-schema-viewer" ng-click="$ctrl.toggleSchemaViewer()">Schema</button>
|
|
3
|
+
<div class="schema-viewer-error" ng-show="$ctrl.loadError">
|
|
4
|
+
<div>
|
|
5
|
+
{{ $ctrl.t('schema.error') }}
|
|
6
|
+
</div>
|
|
7
|
+
<div class="error-details">
|
|
8
|
+
{{ $ctrl.loadError }}
|
|
9
|
+
</div>
|
|
10
|
+
<button class="btn schema-download-retry" ng-click="$ctrl.retryLoadSchema()">{{ $ctrl.t('schema.retry') }}</button>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="schema-viewer-panel">
|
|
13
|
+
<div class="editor-panel-title schema-panel-title">
|
|
14
|
+
<span>{{ $ctrl.t('schema.title') }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="schema-panel-toolbar">
|
|
17
|
+
<input type="text" class="schema-search-input" ng-model="$ctrl.searchTerm" placeholder="{{ $ctrl.t('schema.search_placeholder') }}">
|
|
18
|
+
<div class="schema-panel-note">{{ $ctrl.t('schema.note') }}</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="schema-panel-body">
|
|
21
|
+
<div class="type-definition" id="{{type.kind}}_{{type.name||type.ofType.name}}"
|
|
22
|
+
ng-repeat="type in $ctrl.getFilteredTypes() track by (type.kind + '_' + type.name)">
|
|
23
|
+
<div ng-if="type.kind === 'OBJECT'">
|
|
24
|
+
<span class="keyword">type</span> {{ type.name }} <span class="bracket">{</span>
|
|
25
|
+
<div class="field" ng-repeat="field in type.fields" ng-if="$ctrl.shouldShowField(type, field)">
|
|
26
|
+
<span class="field-name" ng-class="{ 'is-actionable': $ctrl.getOperationForType(type.name) }" ng-mousedown="$ctrl.onFieldMouseDown($event, type, field)" ng-mouseover="$ctrl.showFieldTooltip($event, field)" ng-mouseout="$ctrl.hideTooltip()">{{ field.name }}</span><field-args field-args="field.args" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-args><span class="colon">:</span> <field-type field-ref="field" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
|
|
27
|
+
<!--<span ng-if="field.type.kind==='LIST'" class="array-bracket">[</span><a href="#{{field.type.kind}}_{{field.type.name}}" ng-click="$ctrl.onTypeAnchorClick($event)"><span class="field-type">{{ field.type.name || field.type.ofType.name }}</span></a><span ng-if="field.type.kind==='LIST'" class="array-bracket">]</span> -->
|
|
28
|
+
</div>
|
|
29
|
+
<span class="bracket">}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div ng-if="type.kind === 'ENUM'">
|
|
32
|
+
<span class="keyword">enum</span> {{ type.name }} <span class="bracket">{</span>
|
|
33
|
+
<div class="field" ng-repeat="value in type.enumValues">
|
|
34
|
+
<span class="enum-name" ng-mouseover="$ctrl.showEnumTooltip($event, value)" ng-mouseout="$ctrl.hideTooltip()">{{ value.name }}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<span class="bracket">}</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div ng-if="type.kind === 'INTERFACE'">
|
|
39
|
+
<span class="keyword">interface</span> {{ type.name }} <span class="bracket">{</span>
|
|
40
|
+
<div class="field" ng-repeat="field in type.fields" ng-if="$ctrl.shouldShowField(type, field)">
|
|
41
|
+
<span class="field-name" ng-class="{ 'is-actionable': $ctrl.getOperationForType(type.name) }" ng-mousedown="$ctrl.onFieldMouseDown($event, type, field)" ng-mouseover="$ctrl.showFieldTooltip($event, field)" ng-mouseout="$ctrl.hideTooltip()">{{ field.name }}</span><field-args field-args="field.args" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-args><span class="colon">:</span> <field-type field-ref="field" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
|
|
42
|
+
</div>
|
|
43
|
+
<span class="bracket">}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div ng-if="type.kind === 'UNION'">
|
|
46
|
+
<span class="keyword">union</span> {{ type.name }} <span class="equal">=</span>
|
|
47
|
+
<div class="field" ng-repeat="possibleType in type.possibleTypes" ng-if="$ctrl.shouldShowField(type, possibleType)">
|
|
48
|
+
<span class="field-name" ng-mouseover="$ctrl.showPossibleTypeTooltip($event, possibleType)" ng-mouseout="$ctrl.hideTooltip()">{{ possibleType.name }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div ng-if="type.kind === 'INPUT_OBJECT'">
|
|
52
|
+
<span class="keyword">input</span> {{ type.name }} <span class="bracket">{</span>
|
|
53
|
+
<div class="field" ng-repeat="inputField in type.inputFields" ng-if="$ctrl.shouldShowField(type, inputField)">
|
|
54
|
+
<span class="field-name" ng-mouseover="$ctrl.showInputFieldTooltip($event, inputField)" ng-mouseout="$ctrl.hideTooltip()">{{ inputField.name }}</span><span class="colon">:</span> <field-type field-ref="inputField" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
|
|
55
|
+
</div>
|
|
56
|
+
<span class="bracket">}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div ng-if="type.kind === 'SCALAR'">
|
|
59
|
+
<span class="keyword">scalar</span> {{ type.name }}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="schema-tooltip" ng-if="$ctrl.tooltip.visible" ng-style="{ top: $ctrl.tooltip.top + 'px', left: $ctrl.tooltip.left + 'px' }">
|
|
65
|
+
<div class="schema-tooltip-signature">
|
|
66
|
+
<span class="schema-tooltip-name schema-tooltip-name-{{ $ctrl.tooltip.labelClass }}">{{ $ctrl.tooltip.label }}</span>
|
|
67
|
+
<span class="colon" ng-if="$ctrl.tooltip.typeTokens.length">:</span>
|
|
68
|
+
<span class="schema-tooltip-type-tokens" ng-if="$ctrl.tooltip.typeTokens.length">
|
|
69
|
+
<span ng-repeat="token in $ctrl.tooltip.typeTokens track by $index" class="{{ token.className }}">{{ token.text }}</span>
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="schema-tooltip-description" ng-if="$ctrl.tooltip.description">{{ $ctrl.tooltip.description }}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" ng-app="app">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Playground</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles/main.css?v=20260409r">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
|
9
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.css">
|
|
10
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/hint/show-hint.min.css">
|
|
11
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
|
12
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.js"></script>
|
|
13
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/edit/matchbrackets.min.js"></script>
|
|
14
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/hint/show-hint.min.js"></script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
16
|
+
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
|
17
|
+
<script src="./js/main.js?v=20260409p"></script>
|
|
18
|
+
<script src="./js/components/config-editor.js?v=20260409e"></script>
|
|
19
|
+
<script src="./js/components/query-editor.js?v=20260409t"></script>
|
|
20
|
+
<script src="./js/components/variables-editor.js?v=20260409k"></script>
|
|
21
|
+
<script src="./js/components/headers-editor.js?v=20260409i"></script>
|
|
22
|
+
<script src="./js/components/response-viewer.js?v=20260409c"></script>
|
|
23
|
+
<script src="./js/components/schema-viewer.js?v=20260409g"></script>
|
|
24
|
+
</head>
|
|
25
|
+
<body ng-class="{ 'ready': main.ready }" ng-controller="MainController as main">
|
|
26
|
+
<nav class="top-nav">
|
|
27
|
+
<div class="top-nav-brand">
|
|
28
|
+
<div class="top-nav-title-row">
|
|
29
|
+
<div class="top-nav-title">{{ main.t('app.name') }}</div>
|
|
30
|
+
<span class="top-nav-badge">{{ main.appVersion }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="top-nav-subtitle">{{ main.t('app.developed_by') }}</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="top-nav-actions">
|
|
35
|
+
<label class="locale-label" for="locale-select">{{ main.t('nav.language') }}</label>
|
|
36
|
+
<select id="locale-select" class="locale-select" ng-model="main.locale" ng-change="main.setLocale(main.locale)" ng-options="locale.code as main.t(locale.labelKey) for locale in main.locales"></select>
|
|
37
|
+
<button class="config-btn" ng-click="main.openConfig()">{{ main.t('nav.settings') }}</button>
|
|
38
|
+
</div>
|
|
39
|
+
</nav>
|
|
40
|
+
|
|
41
|
+
<div class="editor">
|
|
42
|
+
<div class="input-group">
|
|
43
|
+
<input class="form-control" type="text" ng-model="main.url" placeholder="URL">
|
|
44
|
+
<button ng-click="main.send()">{{ main.t('actions.send') }}</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="tabs">
|
|
47
|
+
<div class="tab-item" ng-repeat="tab in main.tabs track by tab.id" ng-class="{ 'active': main.activeTab === $index }" ng-mousedown="main.handleTabMouseDown($event, $index)">
|
|
48
|
+
<a href="" ng-if="!tab.isEditingTitle" ng-click="main.activeTab = $index" ng-dblclick="main.beginTabTitleEdit($index, $event)">{{ tab.title }}</a>
|
|
49
|
+
<input
|
|
50
|
+
ng-if="tab.isEditingTitle"
|
|
51
|
+
id="tab-title-editor-{{ tab.id }}"
|
|
52
|
+
class="tab-title-input"
|
|
53
|
+
type="text"
|
|
54
|
+
ng-model="tab.titleDraft"
|
|
55
|
+
ng-click="$event.stopPropagation()"
|
|
56
|
+
ng-blur="main.commitTabTitleEdit($index)"
|
|
57
|
+
ng-keydown="main.handleTabTitleKeydown($event, $index)">
|
|
58
|
+
<button type="button" class="tab-close-btn" ng-if="main.tabs.length > 1" ng-click="main.closeTab($index); $event.stopPropagation()" aria-label="Close tab">×</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="add-tab">
|
|
61
|
+
<a href="" ng-click="main.addTab()">+</a>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="tab-content" ng-repeat="tab in main.tabs" ng-show="main.activeTab === $index">
|
|
65
|
+
<div class="query-and-result-container">
|
|
66
|
+
<div class="query-editor">
|
|
67
|
+
<div class="editor-panel-title">
|
|
68
|
+
<span>{{ main.t('editor.query') }}</span>
|
|
69
|
+
<span class="editor-panel-actions">
|
|
70
|
+
<button type="button" class="editor-panel-action" ng-click="tab.queryEditorApi && tab.queryEditorApi.resetToOperation('query')">{{ main.t('actions.new_query') }}</button>
|
|
71
|
+
<button type="button" class="editor-panel-action" ng-click="tab.queryEditorApi && tab.queryEditorApi.resetToOperation('mutation')">{{ main.t('actions.new_mutation') }}</button>
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<query-editor query="tab.query" schema="main.schema" api="tab.queryEditorApi" on-change="main.persistTabs()"></query-editor>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="actions">
|
|
77
|
+
<button type="button" class="send-fab" ng-click="main.send()" aria-label="{{ main.t('actions.send') }}" title="{{ main.t('actions.send') }}">
|
|
78
|
+
▶
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="result">
|
|
82
|
+
<div class="editor-panel-title">{{ main.t('editor.response') }}</div>
|
|
83
|
+
<response-viewer result="tab.result" api="tab.responseViewerApi"></response-viewer>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="variables-and-headers-container">
|
|
87
|
+
<div class="variables-editor">
|
|
88
|
+
<div class="editor-panel-title">
|
|
89
|
+
<span>{{ main.t('editor.variables') }}</span>
|
|
90
|
+
<button type="button" class="editor-panel-action" ng-click="tab.variablesEditorApi && tab.variablesEditorApi.format()">{{ main.t('actions.format') }}</button>
|
|
91
|
+
</div>
|
|
92
|
+
<variables-editor variables="tab.variables" query="tab.query" schema="main.schema" api="tab.variablesEditorApi" on-change="main.persistTabs()"></variables-editor>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="headers-editor">
|
|
95
|
+
<div class="editor-panel-title">
|
|
96
|
+
<span>{{ main.t('editor.headers') }}</span>
|
|
97
|
+
<button type="button" class="editor-panel-action" ng-click="tab.headersEditorApi && tab.headersEditorApi.format()">{{ main.t('actions.format') }}</button>
|
|
98
|
+
</div>
|
|
99
|
+
<headers-editor headers="tab.headers" api="tab.headersEditorApi" on-change="main.persistTabs()"></headers-editor>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<schema-viewer schema="main.schema" load-error="main.loadSchemaError" retry-load-schema="main.loadSchema()" insert-schema-operation="main.insertSchemaOperation(snippet)"></schema-viewer>
|
|
105
|
+
<div class="modal-overlay" ng-show="main.showConfig"></div>
|
|
106
|
+
<div class="modal config-modal" ng-show="main.showConfig">
|
|
107
|
+
<div class="modal-content">
|
|
108
|
+
<h2>{{ main.t('modal.settings') }}</h2>
|
|
109
|
+
<config-editor export-workspace="main.downloadWorkspace()" import-workspace="main.importWorkspace(workspace)"></config-editor>
|
|
110
|
+
<button class="close-btn" ng-click="main.closeConfig()">{{ main.t('actions.close') }}</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|