@sanity/runtime-cli 1.0.2

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.
Files changed (50) hide show
  1. package/README.md +548 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/actions/functions/dev.d.ts +1 -0
  7. package/dist/actions/functions/dev.js +5 -0
  8. package/dist/actions/functions/invoke.d.ts +2 -0
  9. package/dist/actions/functions/invoke.js +17 -0
  10. package/dist/actions/functions/logs.d.ts +1 -0
  11. package/dist/actions/functions/logs.js +14 -0
  12. package/dist/actions/functions/test.d.ts +2 -0
  13. package/dist/actions/functions/test.js +13 -0
  14. package/dist/commands/functions/dev.d.ts +9 -0
  15. package/dist/commands/functions/dev.js +15 -0
  16. package/dist/commands/functions/invoke.d.ts +13 -0
  17. package/dist/commands/functions/invoke.js +25 -0
  18. package/dist/commands/functions/logs.d.ts +9 -0
  19. package/dist/commands/functions/logs.js +14 -0
  20. package/dist/commands/functions/test.d.ts +14 -0
  21. package/dist/commands/functions/test.js +43 -0
  22. package/dist/config.d.ts +6 -0
  23. package/dist/config.js +9 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.js +5 -0
  26. package/dist/server/app.d.ts +3 -0
  27. package/dist/server/app.js +36 -0
  28. package/dist/server/static/api.js +50 -0
  29. package/dist/server/static/components/api-base.js +10 -0
  30. package/dist/server/static/components/app.css +155 -0
  31. package/dist/server/static/components/function-list.js +49 -0
  32. package/dist/server/static/components/network-spinner.js +71 -0
  33. package/dist/server/static/components/payload-panel.js +45 -0
  34. package/dist/server/static/components/response-panel.js +83 -0
  35. package/dist/server/static/index.html +55 -0
  36. package/dist/server/static/sanity-logo-sm.svg +1 -0
  37. package/dist/server/static/vendor/vendor.bundle.js +26857 -0
  38. package/dist/utils/build-payload.d.ts +2 -0
  39. package/dist/utils/build-payload.js +15 -0
  40. package/dist/utils/child-process-wrapper.js +33 -0
  41. package/dist/utils/invoke-local.d.ts +2 -0
  42. package/dist/utils/invoke-local.js +51 -0
  43. package/dist/utils/is-dependency.d.ts +1 -0
  44. package/dist/utils/is-dependency.js +7 -0
  45. package/dist/utils/is-json.d.ts +1 -0
  46. package/dist/utils/is-json.js +12 -0
  47. package/dist/utils/types.d.ts +16 -0
  48. package/dist/utils/types.js +1 -0
  49. package/oclif.manifest.json +179 -0
  50. package/package.json +85 -0
@@ -0,0 +1,25 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { invoke } from '../../actions/functions/invoke.js';
3
+ export default class Invoke extends Command {
4
+ static args = {
5
+ id: Args.string({ description: 'The ID of the function to invoke', required: true }),
6
+ };
7
+ static description = 'Invoke a remote Sanity Function';
8
+ static examples = [
9
+ `<%= config.bin %> <%= command.id %> <ID> --data '{ "id": 1 }'`,
10
+ `<%= config.bin %> <%= command.id %> <ID> --file 'payload.json'`,
11
+ ];
12
+ static flags = {
13
+ data: Flags.string({ char: 'd', description: 'Data to send to the function', required: false }),
14
+ file: Flags.string({
15
+ char: 'f',
16
+ description: 'Read data from file and send to the function',
17
+ required: false,
18
+ }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(Invoke);
22
+ const result = await invoke(args.id, { data: flags.data, file: flags.file });
23
+ this.log(JSON.stringify(result, null, 2));
24
+ }
25
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Logs extends Command {
3
+ static args: {
4
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,14 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { logs } from '../../actions/functions/logs.js';
3
+ export default class Logs extends Command {
4
+ static args = {
5
+ id: Args.string({ description: 'The ID of the function to retrieve logs for', required: true }),
6
+ };
7
+ static description = 'Retrieve logs for a Sanity Function';
8
+ static examples = ['<%= config.bin %> <%= command.id %> <ID>'];
9
+ async run() {
10
+ const { args } = await this.parse(Logs);
11
+ const result = await logs(args.id);
12
+ this.log(JSON.stringify(result, null, 2));
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Test extends Command {
3
+ static args: {
4
+ path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ timeout: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,43 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { testAction } from '../../actions/functions/test.js';
3
+ export default class Test extends Command {
4
+ static args = {
5
+ path: Args.string({ description: 'The path to the function source code', required: true }),
6
+ };
7
+ static description = 'Invoke a local Sanity Function';
8
+ static examples = [
9
+ `<%= config.bin %> <%= command.id %> ./test.ts --data '{ "id": 1 }'`,
10
+ `<%= config.bin %> <%= command.id %> ./test.js --file 'payload.json'`,
11
+ `<%= config.bin %> <%= command.id %> ./test.ts --data '{ "id": 1 }' --timeout 60`,
12
+ ];
13
+ static flags = {
14
+ data: Flags.string({ char: 'd', description: 'Data to send to the function', required: false }),
15
+ file: Flags.string({
16
+ char: 'f',
17
+ description: 'Read data from file and send to the function',
18
+ required: false,
19
+ }),
20
+ timeout: Flags.integer({
21
+ char: 't',
22
+ description: 'Execution timeout value in seconds',
23
+ required: false,
24
+ }),
25
+ };
26
+ async run() {
27
+ const { args, flags } = await this.parse(Test);
28
+ const { json, logs, error } = await testAction(args.path, {
29
+ data: flags.data,
30
+ file: flags.file,
31
+ timeout: flags.timeout,
32
+ });
33
+ if (!error) {
34
+ this.log('Logs:');
35
+ this.log(logs);
36
+ this.log('Response:');
37
+ this.log(JSON.stringify(json, null, 2));
38
+ }
39
+ else {
40
+ this.log(error.toString());
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ declare const _default: {
2
+ server: {
3
+ url: string;
4
+ };
5
+ };
6
+ export default _default;
package/dist/config.js ADDED
@@ -0,0 +1,9 @@
1
+ import { env } from 'node:process';
2
+ const nodeEnv = env.NODE_ENV ?? 'development';
3
+ const isDev = nodeEnv === 'development';
4
+ const baseUrl = isDev ? 'http://localhost:4567' : '';
5
+ export default {
6
+ server: {
7
+ url: baseUrl,
8
+ },
9
+ };
@@ -0,0 +1,5 @@
1
+ import { run } from '@oclif/core';
2
+ import { dev } from './actions/functions/dev.js';
3
+ import { logs } from './actions/functions/logs.js';
4
+ import { testAction } from './actions/functions/test.js';
5
+ export { dev as devAction, logs as logsAction, testAction, run };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { run } from '@oclif/core';
2
+ import { dev } from './actions/functions/dev.js';
3
+ import { logs } from './actions/functions/logs.js';
4
+ import { testAction } from './actions/functions/test.js';
5
+ export { dev as devAction, logs as logsAction, testAction, run };
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export default app;
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { cwd } from 'node:process';
4
+ import { serveStatic } from '@hono/node-server/serve-static';
5
+ import { Hono } from 'hono';
6
+ import invoke from '../utils/invoke-local.js';
7
+ import isDependency from '../utils/is-dependency.js';
8
+ function errorResponse(code, message, details = {}) {
9
+ return { error: { code, details, message } };
10
+ }
11
+ function getStaticPath() {
12
+ return isDependency('./server/static')
13
+ ? './node_modules/@sanity/runtime-cli/dist/server/static'
14
+ : './src/server/static';
15
+ }
16
+ const app = new Hono();
17
+ app.use('*', serveStatic({ root: getStaticPath() }));
18
+ app.get('/blueprint', async (c) => {
19
+ const response = JSON.parse(readFileSync(join(cwd(), './blueprints.json')).toString());
20
+ return c.json(response);
21
+ });
22
+ app.post('/invoke', async (c) => {
23
+ const { data = {}, func } = await c.req.json();
24
+ const response = await invoke(func, { data: JSON.parse(data) });
25
+ return c.json(response);
26
+ });
27
+ app
28
+ .notFound((c) => c.json(errorResponse('NOT_FOUND', 'Not Found', {
29
+ method: c.req.method,
30
+ path: c.req.path,
31
+ }), 404))
32
+ .onError((err, c) => c.json(errorResponse('INTERNAL_SERVER_ERROR', 'Internal Server Error', {
33
+ error: err.message,
34
+ stack: err,
35
+ }), 500));
36
+ export default app;
@@ -0,0 +1,50 @@
1
+ /* eslint-disable n/no-unsupported-features/node-builtins */
2
+ import {Store} from './vendor/vendor.bundle.js'
3
+
4
+ // eslint-disable-next-line new-cap
5
+ const store = Store()
6
+
7
+ export default function API() {
8
+ return {
9
+ blueprint,
10
+ invoke,
11
+ store,
12
+ subscribe: store.subscribe,
13
+ unsubscribe: store.unsubscribe,
14
+ }
15
+ }
16
+
17
+ function invoke(payloadText = '{}') {
18
+ store.inprogress = true
19
+ const start = Date.now()
20
+ const payload = {
21
+ data: payloadText,
22
+ func: store.selectedIndex,
23
+ }
24
+ fetch('/invoke', {
25
+ body: JSON.stringify(payload),
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ method: 'POST',
30
+ })
31
+ .then((response) => response.json())
32
+ .then((data) => {
33
+ store.inprogress = false
34
+ store.result = {
35
+ ...data,
36
+ time: Date.now() - start,
37
+ }
38
+ })
39
+ }
40
+
41
+ function blueprint() {
42
+ fetch('/blueprint')
43
+ .then((response) => response.json())
44
+ .then((json) => {
45
+ const functions = json?.resources.filter((resource) => resource.kind === 'function')
46
+
47
+ store.functions = functions
48
+ store.selectedIndex = functions[0].src
49
+ })
50
+ }
@@ -0,0 +1,10 @@
1
+ /* globals HTMLElement */
2
+ import apiConstructor from '../api.js'
3
+ const api = apiConstructor()
4
+
5
+ export class ApiBaseElement extends HTMLElement {
6
+ constructor() {
7
+ super()
8
+ this.api = api
9
+ }
10
+ }
@@ -0,0 +1,155 @@
1
+ :root {
2
+ --card-bg-color: light-dark(#f6f6f8, #0d0e12);
3
+ --card-border-color: light-dark(#dbdce2, #252837);
4
+ --text-color: light-dark(#242736, #e3e4e8);
5
+ --button-text-color: light-dark(white, #0d0e12);
6
+ --button-background-color: light-dark(rgb(85, 107, 252), rgb(120, 152, 255));
7
+ --button-background-color-hover: light-dark(rgb(64, 67, 231), rgb(170, 193, 255));
8
+ --button-border-color: light-dark(rgb(85, 107, 252), rgb(120, 152, 255));
9
+ --button-border-color-hover: light-dark(rgb(64, 67, 231), rgb(170, 193, 255));
10
+ }
11
+ html {
12
+ color-scheme: light dark;
13
+ }
14
+
15
+ body {
16
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Liberation Sans', Helvetica, Arial, system-ui, sans-serif;
17
+ background-color: var(--card-bg-color);
18
+ color: var(--text-color) !important;
19
+ min-height: 100vh;
20
+ display: grid;
21
+ grid-template-areas:
22
+ 'header'
23
+ 'left-sidebar'
24
+ 'main'
25
+ 'footer';
26
+ grid-template-rows: min-content min-content 1fr min-content;
27
+ }
28
+ body > header {
29
+ grid-area: header;
30
+ }
31
+ body > nav {
32
+ grid-area: left-sidebar;
33
+ }
34
+ body > main {
35
+ grid-area: main;
36
+ }
37
+ body > footer {
38
+ grid-area: footer;
39
+ }
40
+
41
+ @media (min-width: 40rem) {
42
+ body {
43
+ grid-template:
44
+ 'header header' min-content
45
+ 'left-sidebar main ' 1fr
46
+ 'footer footer' min-content
47
+ / minmax(auto, var(--layout-max-sidebar-width, 16rem)) minmax(var(--layout-min-content-width, 16rem), 1fr);
48
+ }
49
+ }
50
+
51
+ @media (max-width: 40rem) {
52
+ button {
53
+ width: 100%;
54
+ }
55
+
56
+ .block-lg {
57
+ display: block !important;
58
+ }
59
+
60
+ .hidden-lg {
61
+ display: none;
62
+ }
63
+ }
64
+
65
+ header {
66
+ border-bottom: 1px solid var(--card-border-color);
67
+ }
68
+
69
+ footer {
70
+ border-top: 1px solid var(--card-border-color);
71
+ }
72
+
73
+ .logo {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.25rem;
77
+ }
78
+
79
+ .logo-image {
80
+ height: 2.25em;
81
+ width: auto;
82
+ }
83
+
84
+ .logo-image img {
85
+ width: 100%;
86
+ height: 100%;
87
+ }
88
+
89
+ .logo-text {
90
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Liberation Sans', Helvetica, Arial, system-ui, sans-serif;
91
+ font-weight: 500;
92
+ margin: 0px;
93
+ line-height: calc(1.46154);
94
+ letter-spacing: 0px;
95
+ }
96
+
97
+ .footer-text {
98
+ position: relative;
99
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Liberation Sans', Helvetica, Arial, system-ui, sans-serif;
100
+ font-weight: 400;
101
+ padding: 1px 0px;
102
+ margin: 0px;
103
+ font-size: 0.8125rem;
104
+ line-height: calc(1.46154);
105
+ letter-spacing: 0px;
106
+ transform: translateY(0.3125rem);
107
+ }
108
+
109
+ .sanity-button {
110
+ color: var(--button-text-color) !important;
111
+ background-color: var(--button-background-color) !important;
112
+ border-color: var(--button-border-color) !important;
113
+ }
114
+
115
+ .sanity-button:hover {
116
+ background-color: var(--button-background-color-hover) !important;
117
+ border-color: var(--button-border-color-hover) !important;
118
+ }
119
+
120
+ .selected {
121
+ color: var(--button-text-color);
122
+ background-color: var(--button-background-color);
123
+ }
124
+
125
+ ol[type='content'] li:hover {
126
+ color: var(--button-text-color);
127
+ background-color: var(--button-background-color-hover) !important;
128
+ }
129
+
130
+ m-box,
131
+ m-tabs {
132
+ background-color: var(--card-bg-color) !important;
133
+ border-color: var(--card-border-color) !important;
134
+ color: var(--text-color) !important;
135
+ }
136
+
137
+ m-tabs {
138
+ & > :is(a, button) {
139
+ color: var(--text-color) !important;
140
+ }
141
+ }
142
+
143
+ m-tabs {
144
+ & > :is(a, button) {
145
+ &[aria-selected='true'] {
146
+ border-color: var(--button-border-color) !important;
147
+ }
148
+ }
149
+ }
150
+
151
+ #payload,
152
+ #response {
153
+ color: black;
154
+ background-color: white !important;
155
+ }
@@ -0,0 +1,49 @@
1
+ /* globals customElements */
2
+ import {ApiBaseElement} from './api-base.js'
3
+
4
+ const template = `<ol class="hidden-lg" type="content"></ol>
5
+ <fieldset class="pad-sm hidden block-lg"><select></select></fieldset>
6
+ `
7
+
8
+ class FunctionList extends ApiBaseElement {
9
+ functionClicked = (event) => {
10
+ // eslint-disable-next-line unicorn/prefer-dom-node-text-content
11
+ const target = this.api.store.functions.find((func) => func.name === event.srcElement.innerText)
12
+ this.api.store.selectedIndex = target.src
13
+ }
14
+ functionSelected = (event) => {
15
+ this.api.store.selectedIndex = event.srcElement.value
16
+ }
17
+ renderFunctions = () => {
18
+ this.list.innerHTML = this.api.store.functions
19
+ .map((func) => {
20
+ const selected = this.api.store.selectedIndex === func.src ? 'selected' : ''
21
+ return `<li class="pad-sm ${selected}">${func.name}</li>`
22
+ })
23
+ .join('')
24
+ this.select.innerHTML = this.api.store.functions
25
+ .map((func) => {
26
+ const selected = this.api.store.selectedIndex === func.src ? 'selected' : ''
27
+ return `<option value="${func.src}" ${selected}>${func.name}</option>`
28
+ })
29
+ .join('')
30
+ }
31
+
32
+ connectedCallback() {
33
+ this.innerHTML = template
34
+ this.list = this.querySelector('ol')
35
+ this.select = this.querySelector('select')
36
+ this.list.addEventListener('click', this.functionClicked)
37
+ this.select.addEventListener('change', this.functionSelected)
38
+ this.api.subscribe(this.renderFunctions, ['functions', 'selectedIndex'])
39
+ this.api.blueprint()
40
+ }
41
+
42
+ disconnectedCallback() {
43
+ this.list.removeEventListener('click', this.functionClicked)
44
+ this.select.removeEventListener('change', this.functionSelected)
45
+ this.api.unsubscribe(this.renderFunctions)
46
+ }
47
+ }
48
+
49
+ customElements.define('function-list', FunctionList)
@@ -0,0 +1,71 @@
1
+ /* globals customElements HTMLElement */
2
+ const template = `<style>
3
+ network-spinner {
4
+ --track-width: 2px;
5
+ --track-color: var(--card-border-color);
6
+ --indicator-color: var(--text-color);
7
+ --speed: 2s;
8
+
9
+ display: inline-flex;
10
+ width: 1em;
11
+ height: 1em;
12
+ flex: none;
13
+ }
14
+
15
+ .spinner {
16
+ flex: 1 1 auto;
17
+ height: 100%;
18
+ width: 100%;
19
+ }
20
+
21
+ .spinner__track,
22
+ .spinner__indicator {
23
+ fill: none;
24
+ stroke-width: var(--track-width);
25
+ r: calc(0.5em - var(--track-width) / 2);
26
+ cx: 0.5em;
27
+ cy: 0.5em;
28
+ transform-origin: 50% 50%;
29
+ }
30
+
31
+ .spinner__track {
32
+ stroke: var(--track-color);
33
+ transform-origin: 0% 0%;
34
+ }
35
+
36
+ .spinner__indicator {
37
+ stroke: var(--indicator-color);
38
+ stroke-linecap: round;
39
+ stroke-dasharray: 150% 75%;
40
+ animation: spin var(--speed) linear infinite;
41
+ }
42
+
43
+ @keyframes spin {
44
+ 0% {
45
+ transform: rotate(0deg);
46
+ stroke-dasharray: 0.05em, 3em;
47
+ }
48
+
49
+ 50% {
50
+ transform: rotate(450deg);
51
+ stroke-dasharray: 1.375em, 1.375em;
52
+ }
53
+
54
+ 100% {
55
+ transform: rotate(1080deg);
56
+ stroke-dasharray: 0.05em, 3em;
57
+ }
58
+ }
59
+ </style>
60
+ <svg part="base" class="spinner" role="progressbar" aria-label="loading">
61
+ <circle class="spinner__track"></circle>
62
+ <circle class="spinner__indicator"></circle>
63
+ </svg>`
64
+
65
+ class NetworkSpinner extends HTMLElement {
66
+ connectedCallback() {
67
+ this.innerHTML = template
68
+ }
69
+ }
70
+
71
+ customElements.define('network-spinner', NetworkSpinner)
@@ -0,0 +1,45 @@
1
+ /* globals customElements */
2
+ import {EditorView, basicSetup, json} from '../vendor/vendor.bundle.js'
3
+ import {ApiBaseElement} from './api-base.js'
4
+
5
+ const template = `<m-box>
6
+ <h2 class="mar-t-0">Payload</h2>
7
+ <div id="payload" name="payload"></div>
8
+ <button ord="primary" class="mar-t-sm sanity-button">Invoke</button>
9
+ </m-box>
10
+ `
11
+ class PayloadPanel extends ApiBaseElement {
12
+ invoke = () => {
13
+ const payloadText = this.api.store.payload.state.doc.text.join('') || '{}'
14
+ this.api.invoke(payloadText)
15
+ }
16
+ updateButtonText = ({inprogress}) => {
17
+ if (inprogress) {
18
+ this.button.setAttribute('disabled', '')
19
+ this.button.innerHTML = '<network-spinner></network-spinner>'
20
+ } else {
21
+ this.button.removeAttribute('disabled')
22
+ this.button.innerText = 'Invoke'
23
+ }
24
+ }
25
+
26
+ connectedCallback() {
27
+ this.innerHTML = template
28
+ this.payload = this.querySelector('#payload')
29
+ this.button = this.querySelector('button')
30
+ this.button.addEventListener('click', this.invoke)
31
+ this.api.subscribe(this.updateButtonText, ['inprogress'])
32
+
33
+ this.api.store.payload = new EditorView({
34
+ doc: '\n\n\n\n',
35
+ extensions: [basicSetup, json()],
36
+ parent: this.payload,
37
+ })
38
+ }
39
+
40
+ disconnectedCallback() {
41
+ this.button.removeEventListener('click', this.invoke)
42
+ }
43
+ }
44
+
45
+ customElements.define('payload-panel', PayloadPanel)
@@ -0,0 +1,83 @@
1
+ /* eslint-disable unicorn/prefer-dom-node-text-content */
2
+ /* globals customElements document */
3
+ import {
4
+ EditorState,
5
+ EditorView,
6
+ basicSetup,
7
+ json,
8
+ prettyBytes,
9
+ prettyMilliseconds,
10
+ } from '../vendor/vendor.bundle.js'
11
+ import {ApiBaseElement} from './api-base.js'
12
+
13
+ const template = `<m-box>
14
+ <m-tabs role="tablist">
15
+ <button id="a" role="tab" aria-selected="true">Response</button>
16
+ <button id="b" role="tab">Console</button>
17
+ </m-tabs>
18
+ <div role="tabpanel" data-tab-id="a" class="pad-t-sm">
19
+ <div class="mar-b-sm">
20
+ <span id="time"></span> <span id="size"></span>
21
+ </div>
22
+ <div id="response" name="response"></div>
23
+ </div>
24
+ <div role="tabpanel" data-tab-id="b" class="pad-t-sm" hidden><pre></pre></div>
25
+ </m-box>
26
+ `
27
+ class ResponsePanel extends ApiBaseElement {
28
+ switchTab = (e) => {
29
+ const selectedTabId = e.target.closest('[role=tab]').id
30
+
31
+ // Select the tab and its panel
32
+ for (const tab of e.currentTarget.querySelectorAll('[role=tab]')) {
33
+ tab.ariaSelected = tab.id === selectedTabId
34
+ }
35
+
36
+ for (const panel of document.querySelectorAll('[role=tabpanel')) {
37
+ panel.hidden = panel.dataset.tabId !== selectedTabId
38
+ }
39
+ }
40
+ updateResponse = ({result}) => {
41
+ const {error, json, logs, time} = result
42
+ if (!error) {
43
+ const transaction = this.api.store.response.state.update({
44
+ changes: {
45
+ from: 0,
46
+ insert: JSON.stringify(json, null, 2),
47
+ to: this.api.store.response.state.doc.length,
48
+ },
49
+ })
50
+ this.api.store.response.dispatch(transaction)
51
+
52
+ this.size.innerText = json ? prettyBytes(JSON.stringify(json).length) : ''
53
+ this.time.innerText = prettyMilliseconds(time)
54
+ this.consoleTab.innerText = logs
55
+ } else {
56
+ this.consoleTab.innerText = error?.details?.error
57
+ }
58
+ }
59
+
60
+ connectedCallback() {
61
+ this.innerHTML = template
62
+ this.response = this.querySelector('#response')
63
+ this.size = this.querySelector('#size')
64
+ this.time = this.querySelector('#time')
65
+ this.consoleTab = this.querySelector('pre')
66
+ this.tabs = this.querySelector('m-tabs')
67
+ this.tabs.addEventListener('click', this.switchTab)
68
+ this.api.subscribe(this.updateResponse, ['result'])
69
+
70
+ this.api.store.response = new EditorView({
71
+ doc: '\n\n\n\n',
72
+ extensions: [basicSetup, json(), EditorState.readOnly.of(true)],
73
+ parent: this.response,
74
+ })
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ this.tabs.removeEventListener('click', this.switchTab)
79
+ this.api.unsubscribe(this.updateResponse)
80
+ }
81
+ }
82
+
83
+ customElements.define('response-panel', ResponsePanel)