mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
1 Commits
feature/so
...
bugfix/res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14645d1d5f |
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
run: npm i
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
"packages/bruno-testbench"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
@@ -18,15 +17,10 @@
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:chrome-extension": "./scripts/build-chrome-extension.sh",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
if (!isServer) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -16,8 +15,7 @@
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/schema": "0.1.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -32,20 +30,16 @@
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.3",
|
||||
"next": "12.3.1",
|
||||
"path": "^0.12.7",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tabs": "^3.2.3",
|
||||
"reckonjs": "^0.1.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord } from '@tabler/icons';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
@@ -14,12 +14,6 @@ const BrunoSupport = ({ onClose }) => {
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
|
||||
@@ -92,6 +92,8 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
|
||||
@@ -12,7 +12,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -29,7 +29,6 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
.react-tabs__tab-list {
|
||||
border-bottom: none !important;
|
||||
padding-top: 0;
|
||||
padding-left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
|
||||
.react-tabs__tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
user-select: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
color: rgb(125 125 125);
|
||||
outline: none !important;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@@ -19,12 +27,36 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
&:after {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
border: none;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
border: none;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,133 +1,26 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import React from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
loadSchema();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(onSchemaLoad) {
|
||||
onSchemaLoad(schema);
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
const onQueryChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
value={query}
|
||||
onRun={onRun}
|
||||
onEdit={onQueryChange}
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occured!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? (
|
||||
<IconLoader2 className="animate-spin" size={18} strokeWidth={1.5}/>
|
||||
) : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5}/> : null }
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5}/> : null }
|
||||
<span className='ml-1'>{schema ? 'Schema' : 'Load Schema'}</span>
|
||||
<StyledWrapper className="h-full">
|
||||
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
|
||||
<TabList>
|
||||
<Tab tabIndex="-1">Query</Tab>
|
||||
<Tab tabIndex="-1">Headers</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<div className="mt-4">
|
||||
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center cursor-pointer hover:underline ml-2'
|
||||
onClick={toggleDocs}
|
||||
>
|
||||
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<RequestHeaders />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const fetchSchema = (endpoint) => {
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
});
|
||||
}
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [schema, setSchema] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if(!saved) {
|
||||
return null;
|
||||
}
|
||||
return buildClientSchema(JSON.parse(saved));
|
||||
} catch {
|
||||
localStorage.setItem(localStorageKey, null);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchSchema(endpoint)
|
||||
.then((res) => res.json())
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('Graphql Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occured while loading Graphql Schema');
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schema,
|
||||
loadSchema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
||||
@@ -12,7 +12,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color};;
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -29,7 +29,6 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -5,30 +5,12 @@ const StyledWrapper = styled.div`
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
height: calc(100vh - 250px);
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -38,7 +38,6 @@ export default class QueryEditor extends React.Component {
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
@@ -76,51 +75,54 @@ export default class QueryEditor extends React.Component {
|
||||
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-C': () => {
|
||||
if (this.props.onCopyQuery) {
|
||||
this.props.onCopyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-P': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
|
||||
|
||||
'Shift-Ctrl-F': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-M': () => {
|
||||
if (this.props.onMergeQuery) {
|
||||
this.props.onMergeQuery();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
}
|
||||
},
|
||||
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent'
|
||||
}
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
@@ -147,10 +149,6 @@ export default class QueryEditor extends React.Component {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -166,7 +164,7 @@ export default class QueryEditor extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full"
|
||||
className="h-full"
|
||||
aria-label="Query Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
@@ -175,11 +173,8 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_onKeyUp = (_cm, e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
|
||||
_onKeyUp = (_cm, event) => {
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
|
||||
this.editor.execCommand('autocomplete');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const HttpMethodSelector = ({ method, onMethodSelect }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex w-full items-center pl-3 py-1 select-none uppercase">
|
||||
<div className="flex-grow font-medium" id="create-new-request-method">{method}</div>
|
||||
<div className="flex-grow font-medium">{method}</div>
|
||||
<div>
|
||||
<IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
|
||||
@@ -25,25 +25,6 @@ const StyledWrapper = styled.div`
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
|
||||
div.graphql-docs-explorer-container {
|
||||
background: white;
|
||||
outline: none;
|
||||
box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
z-index: 2000;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
|
||||
div.doc-explorer-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.doc-explorer-rhs {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
@@ -12,14 +12,10 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
const DEFAULT_PADDING = 5;
|
||||
|
||||
const RequestTabPanel = () => {
|
||||
if (typeof window == 'undefined') {
|
||||
return <div></div>;
|
||||
@@ -33,43 +29,23 @@ const RequestTabPanel = () => {
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 so that request pane is relatively smaller
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - 5);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
// Not a recommended pattern here to have the child component
|
||||
// make a callback to set state, but treating this as an exception
|
||||
const docExplorerRef = useRef(null);
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [showGqlDocs, setShowGqlDocs] = useState(false);
|
||||
const onSchemaLoad = (schema) => setSchema(schema);
|
||||
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
|
||||
const handleGqlClickReference = (reference) => {
|
||||
if(docExplorerRef.current) {
|
||||
docExplorerRef.current.showDocForReference(reference);
|
||||
}
|
||||
if(!showGqlDocs) {
|
||||
setShowGqlDocs(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
|
||||
setLeftPaneWidth(leftPaneWidth);
|
||||
}, [screenWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - 5);
|
||||
}, [screenWidth, asideWidth, leftPaneWidth]);
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
let leftPaneXPosition = e.clientX + 2;
|
||||
if (leftPaneXPosition < (asideWidth+ DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH) || leftPaneXPosition > (screenWidth - MIN_RIGHT_PANE_WIDTH )) {
|
||||
return;
|
||||
}
|
||||
setLeftPaneWidth(leftPaneXPosition- asideWidth);
|
||||
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
|
||||
setLeftPaneWidth(e.clientX - asideWidth - 5);
|
||||
setRightPaneWidth(screenWidth - e.clientX - 5);
|
||||
}
|
||||
};
|
||||
const handleMouseUp = (e) => {
|
||||
@@ -79,7 +55,7 @@ const RequestTabPanel = () => {
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
|
||||
requestPaneWidth: e.clientX - asideWidth - 5
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -89,6 +65,11 @@ const RequestTabPanel = () => {
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
let schema = null;
|
||||
// let {
|
||||
// schema
|
||||
// } = useGraphqlSchema('https://api.spacex.land/graphql');
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@@ -124,23 +105,24 @@ const RequestTabPanel = () => {
|
||||
})
|
||||
);
|
||||
};
|
||||
const onGraphqlQueryChange = (value) => {};
|
||||
const runQuery = async () => {};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
|
||||
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
</div>
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section className="main flex flex-grow pb-4">
|
||||
<section className="request-pane">
|
||||
<div className="px-4" style={{ width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`, height: `calc(100% - ${DEFAULT_PADDING}px)` }}>
|
||||
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
|
||||
{item.type === 'graphql-request' ? (
|
||||
<GraphQLRequestPane
|
||||
item={item}
|
||||
collection={collection}
|
||||
onRunQuery={runQuery}
|
||||
schema={schema}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
onSchemaLoad={onSchemaLoad}
|
||||
toggleDocs={toggleDocs}
|
||||
handleGqlClickReference={handleGqlClickReference}
|
||||
value={item.request.body.graphql.query}
|
||||
onQueryChange={onGraphqlQueryChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -156,22 +138,8 @@ const RequestTabPanel = () => {
|
||||
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{item.type === 'graphql-request' ? (
|
||||
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
|
||||
<DocExplorer schema={schema} ref={(r) => docExplorerRef.current = r}>
|
||||
<button
|
||||
className='mr-2'
|
||||
onClick={toggleDocs}
|
||||
aria-label="Close Documentation Explorer"
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
</DocExplorer>
|
||||
</div>
|
||||
): null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabPanel;
|
||||
export default RequestTabPanel;
|
||||
|
||||
@@ -119,7 +119,7 @@ const RequestTabs = () => {
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<li className="select-none short-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
|
||||
@@ -70,7 +70,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.is-sidebar-dragging .collection-item-name {
|
||||
&.is-dragging .collection-item-name {
|
||||
cursor: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,12 +2,10 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -17,7 +15,6 @@ import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -25,7 +22,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const isDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
@@ -35,29 +32,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'COLLECTION_ITEM',
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
drop: (draggedItem) => {
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
}
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setItemisCollapsed(false);
|
||||
@@ -95,8 +69,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -116,7 +89,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
'is-dragging': isDragging
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
@@ -141,7 +114,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
||||
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className={itemRowClassName}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
|
||||
@@ -2,11 +2,9 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import filter from 'lodash/filter';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -73,21 +71,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
|
||||
const isLocal = isLocalCollection(collection);
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
drop: (draggedItem) => {
|
||||
console.log('drop', draggedItem);
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
// todo need to make sure that draggedItem belongs to the collection
|
||||
return true;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
|
||||
@@ -96,7 +79,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showRemoveCollectionFromWSModal && <RemoveCollectionFromWorkspace collection={collection} onClose={() => setShowRemoveCollectionFromWSModal(false)} />}
|
||||
{showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />}
|
||||
{showRemoveLocalCollectionModal && <RemoveLocalCollection collection={collection} onClose={() => setShowRemoveLocalCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center" ref={(node) => drop(node)}>
|
||||
<div className="flex py-1 collection-name items-center">
|
||||
<div className="flex flex-grow items-center" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
|
||||
<div className="ml-1" id="sidebar-collection-name">{collection.name}</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import Collection from './Collection';
|
||||
@@ -28,11 +26,7 @@ const Collections = ({ searchText }) => {
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collectionToDisplay && collectionToDisplay.length
|
||||
? collectionToDisplay.map((c) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} />;
|
||||
</DndProvider>
|
||||
);
|
||||
return <Collection searchText={searchText} collection={c} key={c.uid} />;
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections'
|
||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
@@ -43,8 +42,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab({type: values.requestType})
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
@@ -79,27 +77,27 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<div className="hidden">
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="http-request"
|
||||
id="http"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="http-request"
|
||||
value="http"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
Http
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql-request"
|
||||
id="graphql"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
@@ -107,16 +105,16 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="graphql-request"
|
||||
value="graphql"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
Graphql
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
|
||||
@@ -54,7 +54,7 @@ const Wrapper = styled.div`
|
||||
&:hover div.drag-request-border {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar};
|
||||
border-left: solid 1px var(--color-request-dragbar-background-active);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -124,7 +124,7 @@ const Sidebar = () => {
|
||||
title="GitHub"
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.3.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.2.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,14 +11,13 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.collection-options {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.label {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -65,15 +65,15 @@ const Welcome = () => {
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<div className="flex items-center">
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="create-collection">
|
||||
<span className="label ml-2" id="create-collection" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
<div className="flex items-center ml-6">
|
||||
<IconFiles size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="add-collection">
|
||||
<span className="label ml-2" id="add-collection" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
Add Collection to Workspace
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -22,10 +22,6 @@ const GlobalStyle = createGlobalStyle`
|
||||
padding: .215rem .6rem .215rem .6rem;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: .2rem .4rem .2rem .4rem;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: .4rem 1.1rem;
|
||||
line-height: 1.47;
|
||||
|
||||
44
packages/bruno-app/src/hooks/useGraphqlSchema/index.js
Normal file
44
packages/bruno-app/src/hooks/useGraphqlSchema/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoaded(true);
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
schema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
||||
@@ -21,6 +21,17 @@ const Wrapper = styled.div`
|
||||
.fw-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.react-tabs {
|
||||
.react-tabs__tab-list {
|
||||
padding-left: 1rem;
|
||||
border-bottom: 1px solid #cfcfcf;
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { AppProvider } from 'providers/App';
|
||||
import { ToastProvider } from 'providers/Toaster';
|
||||
@@ -10,6 +9,7 @@ import ThemeProvider from 'providers/Theme/index';
|
||||
import '../styles/app.scss';
|
||||
import '../styles/globals.css';
|
||||
import 'tailwindcss/dist/tailwind.min.css';
|
||||
import 'react-tabs/style/react-tabs.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
|
||||
@@ -28,16 +28,6 @@ function NoSsr({ children }) {
|
||||
}
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [domLoaded, setDomLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDomLoaded(true);
|
||||
}, []);
|
||||
|
||||
if(!domLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useIdb from './useIdb';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import useLocalCollectionTreeSync from './useLocalCollectionTreeSync';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
@@ -10,7 +9,6 @@ export const AppContext = React.createContext();
|
||||
|
||||
export const AppProvider = (props) => {
|
||||
useIdb();
|
||||
useTelemetry();
|
||||
useLocalCollectionTreeSync();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import getConfig from 'next/config';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import platformLib from 'platform';
|
||||
import { uuid } from 'utils/common';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR';
|
||||
let posthogClient = null;
|
||||
|
||||
const isPlaywrightTestRunning = () => {
|
||||
return publicRuntimeConfig.PLAYWRIGHT ? true : false;
|
||||
};
|
||||
|
||||
// Todo support chrome and firefox extension
|
||||
const getPlatform = () => {
|
||||
return isElectron() ? 'electron' : 'web';
|
||||
};
|
||||
|
||||
const getPosthogClient = () => {
|
||||
if(posthogClient) {
|
||||
return posthogClient;
|
||||
}
|
||||
|
||||
posthogClient = new PostHog(posthogApiKey);
|
||||
return posthogClient;
|
||||
};
|
||||
|
||||
const getAnonymousTrackingId = () => {
|
||||
let id = localStorage.getItem('bruno.anonymousTrackingId');
|
||||
|
||||
if(!id || !id.length || id.length !== 21) {
|
||||
id = uuid();
|
||||
localStorage.setItem('bruno.anonymousTrackingId', id);
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const trackStart = () => {
|
||||
if(isPlaywrightTestRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackingId = getAnonymousTrackingId();
|
||||
const platform = getPlatform();
|
||||
const client = getPosthogClient();
|
||||
client.capture({
|
||||
distinctId: trackingId,
|
||||
event: 'start',
|
||||
properties: {
|
||||
platform: platform,
|
||||
os: platformLib.os.family
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const useTelemetry = () => {
|
||||
useEffect(() => {
|
||||
trackStart();
|
||||
setInterval(trackStart , 24 * 60 * 60 * 1000);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useTelemetry;
|
||||
@@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 222,
|
||||
leftMenuBarOpen: false,
|
||||
leftSidebarWidth: 270,
|
||||
leftMenuBarOpen: true,
|
||||
screenWidth: 500,
|
||||
showHomePage: false
|
||||
};
|
||||
|
||||
@@ -7,8 +7,6 @@ import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
findItemInCollection,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
findCollectionByUid,
|
||||
recursivelyGetAllItemUids,
|
||||
transformCollectionToSaveToIdb,
|
||||
@@ -18,8 +16,7 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
interpolateEnvironmentVars,
|
||||
getDefaultRequestPaneTab
|
||||
interpolateEnvironmentVars
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
@@ -35,8 +32,6 @@ import {
|
||||
renameItem as _renameItem,
|
||||
cloneItem as _cloneItem,
|
||||
deleteItem as _deleteItem,
|
||||
moveItem as _moveItem,
|
||||
moveItemToRootOfCollection as _moveItemToRootOfCollection,
|
||||
saveRequest as _saveRequest,
|
||||
addEnvironment as _addEnvironment,
|
||||
renameEnvironment as _renameEnvironment,
|
||||
@@ -113,8 +108,7 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: requestItem.uid,
|
||||
collectionUid: newCollection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(requestItem)
|
||||
collectionUid: newCollection.uid
|
||||
})
|
||||
)
|
||||
)
|
||||
@@ -554,121 +548,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
if (isLocalCollection(collection)) {
|
||||
const draggedItem = findItemInCollection(collection, draggedItemUid);
|
||||
const targetItem = findItemInCollection(collection, targetItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
if (!targetItem) {
|
||||
return reject(new Error('Target item not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:move-item', draggedItem.pathname, targetItem.pathname)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
if (!targetItem) {
|
||||
return reject(new Error('Target item not found'));
|
||||
}
|
||||
|
||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||
|
||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||
|
||||
collectionSchema
|
||||
.validate(collectionToSave)
|
||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
_moveItem({
|
||||
collectionUid: collectionUid,
|
||||
draggedItemUid: draggedItemUid,
|
||||
targetItemUid: targetItemUid
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
if (isLocalCollection(collection)) {
|
||||
const draggedItem = findItemInCollection(collection, draggedItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:move-item-to-root-of-collection', draggedItem.pathname)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return reject(new Error('Dragged item not found'));
|
||||
}
|
||||
|
||||
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
|
||||
|
||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||
|
||||
collectionSchema
|
||||
.validate(collectionToSave)
|
||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
_moveItemToRootOfCollection({
|
||||
collectionUid: collectionUid,
|
||||
draggedItemUid: draggedItemUid
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;
|
||||
|
||||
@@ -756,8 +635,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
addDepth,
|
||||
moveCollectionItem,
|
||||
collapseCollection,
|
||||
deleteItemInCollection,
|
||||
isItemARequest
|
||||
@@ -182,38 +181,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
moveItem: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const draggedItemUid = action.payload.draggedItemUid;
|
||||
const targetItemUid = action.payload.targetItemUid;
|
||||
|
||||
if (collection) {
|
||||
const draggedItem = findItemInCollection(collection, draggedItemUid);
|
||||
const targetItem = findItemInCollection(collection, targetItemUid);
|
||||
|
||||
if (!draggedItem || !targetItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveCollectionItem(collection, draggedItem, targetItem);
|
||||
addDepth(collection.items);
|
||||
}
|
||||
},
|
||||
moveItemToRootOfCollection: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const draggedItemUid = action.payload.draggedItemUid;
|
||||
|
||||
if (collection) {
|
||||
const draggedItem = findItemInCollection(collection, draggedItemUid);
|
||||
|
||||
if (!draggedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveCollectionItemToRootOfCollection(collection, draggedItem);
|
||||
addDepth(collection.items);
|
||||
}
|
||||
},
|
||||
requestSent: (state, action) => {
|
||||
const { itemUid, collectionUid, cancelTokenUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -639,22 +606,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestGraphqlQuery: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.body.mode = 'graphql';
|
||||
item.draft.request.body.graphql = item.draft.request.body.graphql || {};
|
||||
item.draft.request.body.graphql.query = action.payload.query;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestMethod: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -818,8 +769,6 @@ export const {
|
||||
deleteItem,
|
||||
renameItem,
|
||||
cloneItem,
|
||||
moveItem,
|
||||
moveItemToRootOfCollection,
|
||||
requestSent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
@@ -842,7 +791,6 @@ export const {
|
||||
deleteMultipartFormParam,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
updateRequestMethod,
|
||||
localCollectionAddFileEvent,
|
||||
localCollectionAddDirectoryEvent,
|
||||
|
||||
@@ -23,7 +23,7 @@ export const tabsSlice = createSlice({
|
||||
uid: action.payload.uid,
|
||||
collectionUid: action.payload.collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
requestPaneTab: 'params',
|
||||
responsePaneTab: 'response'
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
|
||||
@@ -6,8 +6,7 @@ import { ThemeProvider as SCThemeProvider } from 'styled-components';
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
export const ThemeProvider = (props) => {
|
||||
const isBrowserThemeLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'light');
|
||||
|
||||
const theme = themes[storedTheme];
|
||||
const themeOptions = Object.keys(themes);
|
||||
|
||||
@@ -15,14 +15,13 @@ const darkTheme = {
|
||||
},
|
||||
|
||||
menubar: {
|
||||
bg: '#333333'
|
||||
bg: '#333333',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
color: '#ccc',
|
||||
muted: '#9d9d9d',
|
||||
bg: '#252526',
|
||||
dragbar: '#8a8a8a',
|
||||
|
||||
workspace: {
|
||||
bg: '#3D3D3D'
|
||||
@@ -55,8 +54,8 @@ const darkTheme = {
|
||||
},
|
||||
|
||||
dropdown: {
|
||||
color: 'rgb(204, 204, 204)',
|
||||
iconColor: 'rgb(204, 204, 204)',
|
||||
color: "rgb(204, 204, 204)",
|
||||
iconColor: "rgb(204, 204, 204)",
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
hoverBg: '#185387',
|
||||
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
|
||||
@@ -107,7 +106,7 @@ const darkTheme = {
|
||||
active: {
|
||||
bg: 'transparent',
|
||||
hoverBg: 'transparent'
|
||||
}
|
||||
},
|
||||
},
|
||||
gridBorder: '#4f4f4f'
|
||||
}
|
||||
@@ -122,9 +121,9 @@ const darkTheme = {
|
||||
},
|
||||
body: {
|
||||
color: '#ccc',
|
||||
bg: 'rgb(48, 48, 49)'
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
},
|
||||
input: {
|
||||
input : {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
@@ -191,13 +190,10 @@ const darkTheme = {
|
||||
|
||||
table: {
|
||||
border: '#333',
|
||||
thead: {
|
||||
thead : {
|
||||
color: 'rgb(204, 204, 204)'
|
||||
},
|
||||
striped: '#2A2D2F',
|
||||
input: {
|
||||
color: '#555555'
|
||||
}
|
||||
striped: '#2A2D2F'
|
||||
},
|
||||
|
||||
plainGrid: {
|
||||
|
||||
@@ -7,7 +7,7 @@ const lightTheme = {
|
||||
colors: {
|
||||
text: {
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563'
|
||||
muted: '#4b5563',
|
||||
},
|
||||
bg: {
|
||||
danger: '#dc3545'
|
||||
@@ -15,14 +15,13 @@ const lightTheme = {
|
||||
},
|
||||
|
||||
menubar: {
|
||||
bg: 'rgb(44, 44, 44)'
|
||||
bg: 'rgb(44, 44, 44)',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
muted: '#4b5563',
|
||||
bg: '#F3F3F3',
|
||||
dragbar: 'rgb(200, 200, 200)',
|
||||
|
||||
workspace: {
|
||||
bg: '#e1e1e1'
|
||||
@@ -55,8 +54,8 @@ const lightTheme = {
|
||||
},
|
||||
|
||||
dropdown: {
|
||||
color: 'rgb(48 48 48)',
|
||||
iconColor: 'rgb(75, 85, 99)',
|
||||
color: "rgb(48 48 48)",
|
||||
iconColor: "rgb(75, 85, 99)",
|
||||
bg: '#fff',
|
||||
hoverBg: '#e9e9e9',
|
||||
shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',
|
||||
@@ -107,7 +106,7 @@ const lightTheme = {
|
||||
active: {
|
||||
bg: '#dcdcdc',
|
||||
hoverBg: '#dcdcdc'
|
||||
}
|
||||
},
|
||||
},
|
||||
gridBorder: '#f4f4f4'
|
||||
}
|
||||
@@ -126,9 +125,9 @@ const lightTheme = {
|
||||
},
|
||||
body: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
bg: 'white'
|
||||
bg: 'white',
|
||||
},
|
||||
input: {
|
||||
input : {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
@@ -195,13 +194,10 @@ const lightTheme = {
|
||||
|
||||
table: {
|
||||
border: '#efefef',
|
||||
thead: {
|
||||
thead : {
|
||||
color: '#616161'
|
||||
},
|
||||
striped: '#f3f3f3',
|
||||
input: {
|
||||
color: '#000000'
|
||||
}
|
||||
striped: '#f3f3f3'
|
||||
},
|
||||
|
||||
plainGrid: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import reckon from 'reckonjs';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import isString from 'lodash/isString';
|
||||
import map from 'lodash/map';
|
||||
import filter from 'lodash/filter';
|
||||
@@ -122,43 +122,6 @@ export const findEnvironmentInCollection = (collection, envUid) => {
|
||||
return find(collection.environments, (e) => e.uid === envUid);
|
||||
};
|
||||
|
||||
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
} else {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
if (targetItem.type === 'folder') {
|
||||
targetItem.items = targetItem.items || [];
|
||||
targetItem.items.push(draggedItem);
|
||||
} else {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
} else {
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
} else {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
collection.items.push(draggedItem);
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
return map(headers, (header) => {
|
||||
@@ -233,7 +196,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
json: si.draft.request.body.json,
|
||||
text: si.draft.request.body.text,
|
||||
xml: si.draft.request.body.xml,
|
||||
graphql: si.draft.request.body.graphql,
|
||||
multipartForm: si.draft.request.body.multipartForm,
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||
}
|
||||
@@ -251,7 +214,6 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
json: si.request.body.json,
|
||||
text: si.request.body.text,
|
||||
xml: si.request.body.xml,
|
||||
graphql: si.request.body.graphql,
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
}
|
||||
@@ -448,13 +410,3 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
export const getDefaultRequestPaneTab = (item) => {
|
||||
if(item.type === 'http-request') {
|
||||
return 'params';
|
||||
}
|
||||
|
||||
if(item.type === 'graphql-request') {
|
||||
return 'query';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,19 +36,12 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
|
||||
}
|
||||
} else {
|
||||
if(i.request) {
|
||||
let url = '';
|
||||
if(typeof i.request.url === 'string') {
|
||||
url = i.request.url;
|
||||
} else {
|
||||
url = get(i, 'request.url.raw') || '';
|
||||
}
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: i.name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: url,
|
||||
url: get(i, 'request.url.raw'),
|
||||
method: i.request.method,
|
||||
headers: [],
|
||||
params: [],
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import qs from 'qs';
|
||||
import { rawRequest, gql } from 'graphql';
|
||||
import { sendHttpRequestInBrowser } from './browser';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import cancelTokens, { deleteCancelToken } from 'utils/network/cancelTokens';
|
||||
|
||||
export const sendNetworkRequest = async (item, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
if (item.type === 'http-request') {
|
||||
const timeStart = Date.now();
|
||||
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
||||
.then((response) => {
|
||||
@@ -79,15 +79,6 @@ const sendHttpRequest = async (request, options) => {
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
const graphqlQuery = {
|
||||
query: get(request, 'body.graphql.query'),
|
||||
variables: JSON.parse(get(request, 'body.graphql.variables') || '{}')
|
||||
};
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
axiosRequest.data = graphqlQuery;
|
||||
}
|
||||
|
||||
console.log('>>> Sending Request');
|
||||
console.log(axiosRequest);
|
||||
|
||||
@@ -99,6 +90,21 @@ const sendHttpRequest = async (request, options) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sendGraphqlRequest = async (request) => {
|
||||
const query = gql`
|
||||
${request.request.body.graphql.query}
|
||||
`;
|
||||
|
||||
const { data, errors, extensions, headers, status } = await rawRequest(request.request.url, query);
|
||||
|
||||
return {
|
||||
data,
|
||||
headers,
|
||||
data,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||
if (isElectron()) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^21.1.1",
|
||||
"electron-builder": "23.3.3",
|
||||
"electron-builder": "23.0.2",
|
||||
"electron-icon-maker": "^0.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
22
packages/bruno-graphql-docs/.gitignore
vendored
22
packages/bruno-graphql-docs/.gitignore
vendored
@@ -1,22 +0,0 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# production
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "@usebruno/graphql-docs",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/react": "^18.0.25",
|
||||
"graphql": "^16.6.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.6.0",
|
||||
"markdown-it": "^13.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# bruno-graphql-docs
|
||||
|
||||
Standalone graphql docs explorer module forked from [graphiql](https://github.com/graphql/graphiql)
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
const { nodeResolve } = require("@rollup/plugin-node-resolve");
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
const typescript = require("@rollup/plugin-typescript");
|
||||
const dts = require("rollup-plugin-dts");
|
||||
const postcss = require("rollup-plugin-postcss");
|
||||
const { terser } = require("rollup-plugin-terser");
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: "src/index.ts",
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: "cjs",
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: "esm",
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
postcss({
|
||||
minimize: true,
|
||||
extensions: ['.css']
|
||||
}),
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.css']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: "./tsconfig.json" }),
|
||||
terser()
|
||||
],
|
||||
external: ["react", "react-dom", "index.css"]
|
||||
},
|
||||
{
|
||||
input: "dist/esm/index.d.ts",
|
||||
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
||||
plugins: [dts.default()],
|
||||
}
|
||||
];
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { GraphQLSchema, isType, GraphQLNamedType, GraphQLError } from 'graphql';
|
||||
import { FieldType } from './DocExplorer/types';
|
||||
|
||||
import FieldDoc from './DocExplorer/FieldDoc';
|
||||
import SchemaDoc from './DocExplorer/SchemaDoc';
|
||||
import SearchBox from './DocExplorer/SearchBox';
|
||||
import SearchResults from './DocExplorer/SearchResults';
|
||||
import TypeDoc from './DocExplorer/TypeDoc';
|
||||
|
||||
type NavStackItem = {
|
||||
name: string;
|
||||
title?: string;
|
||||
search?: string;
|
||||
def?: GraphQLNamedType | FieldType;
|
||||
};
|
||||
|
||||
const initialNav: NavStackItem = {
|
||||
name: 'Schema',
|
||||
title: 'Documentation Explorer',
|
||||
};
|
||||
|
||||
type DocExplorerProps = {
|
||||
schema?: GraphQLSchema | null;
|
||||
schemaErrors?: readonly GraphQLError[];
|
||||
children?: ReactNode | null;
|
||||
};
|
||||
|
||||
type DocExplorerState = {
|
||||
navStack: NavStackItem[];
|
||||
};
|
||||
|
||||
/**
|
||||
* DocExplorer
|
||||
*
|
||||
* Shows documentations for GraphQL definitions from the schema.
|
||||
*
|
||||
* Props:
|
||||
*
|
||||
* - schema: A required GraphQLSchema instance that provides GraphQL document
|
||||
* definitions.
|
||||
*
|
||||
* Children:
|
||||
*
|
||||
* - Any provided children will be positioned in the right-hand-side of the
|
||||
* top bar. Typically this will be a "close" button for temporary explorer.
|
||||
*
|
||||
*/
|
||||
export class DocExplorer extends React.Component<
|
||||
DocExplorerProps,
|
||||
DocExplorerState
|
||||
> {
|
||||
// handleClickTypeOrField: OnClickTypeFunction | OnClickFieldFunction
|
||||
constructor(props: DocExplorerProps) {
|
||||
super(props);
|
||||
|
||||
this.state = { navStack: [initialNav] };
|
||||
}
|
||||
|
||||
shouldComponentUpdate(
|
||||
nextProps: DocExplorerProps,
|
||||
nextState: DocExplorerState,
|
||||
) {
|
||||
return (
|
||||
this.props.schema !== nextProps.schema ||
|
||||
this.state.navStack !== nextState.navStack ||
|
||||
this.props.schemaErrors !== nextProps.schemaErrors
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { schema, schemaErrors } = this.props;
|
||||
const navStack = this.state.navStack;
|
||||
const navItem = navStack[navStack.length - 1];
|
||||
|
||||
let content;
|
||||
if (schemaErrors) {
|
||||
content = (
|
||||
<div className="error-container">{'Error fetching schema'}</div>
|
||||
);
|
||||
} else if (schema === undefined) {
|
||||
// Schema is undefined when it is being loaded via introspection.
|
||||
content = (
|
||||
<div className="spinner-container">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
);
|
||||
} else if (!schema) {
|
||||
// Schema is null when it explicitly does not exist, typically due to
|
||||
// an error during introspection.
|
||||
content = <div className="error-container">{'No Schema Available'}</div>;
|
||||
} else if (navItem.search) {
|
||||
content = (
|
||||
<SearchResults
|
||||
searchValue={navItem.search}
|
||||
withinType={navItem.def as GraphQLNamedType}
|
||||
schema={schema}
|
||||
onClickType={this.handleClickType}
|
||||
onClickField={this.handleClickField}
|
||||
/>
|
||||
);
|
||||
} else if (navStack.length === 1) {
|
||||
content = (
|
||||
<SchemaDoc schema={schema} onClickType={this.handleClickType} />
|
||||
);
|
||||
} else if (isType(navItem.def)) {
|
||||
content = (
|
||||
<TypeDoc
|
||||
schema={schema}
|
||||
type={navItem.def}
|
||||
onClickType={this.handleClickType}
|
||||
onClickField={this.handleClickField}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<FieldDoc
|
||||
field={navItem.def as FieldType}
|
||||
onClickType={this.handleClickType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const shouldSearchBoxAppear =
|
||||
navStack.length === 1 ||
|
||||
(isType(navItem.def) && 'getFields' in navItem.def);
|
||||
|
||||
let prevName;
|
||||
if (navStack.length > 1) {
|
||||
prevName = navStack[navStack.length - 2].name;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="graphql-docs-container">
|
||||
<section
|
||||
className="doc-explorer"
|
||||
key={navItem.name}
|
||||
aria-label="Documentation Explorer">
|
||||
<div className="doc-explorer-title-bar">
|
||||
{prevName && (
|
||||
<button
|
||||
className="doc-explorer-back"
|
||||
onClick={this.handleNavBackClick}
|
||||
aria-label={`Go back to ${prevName}`}>
|
||||
{prevName}
|
||||
</button>
|
||||
)}
|
||||
<div className="doc-explorer-title">
|
||||
{navItem.title || navItem.name}
|
||||
</div>
|
||||
<div className="doc-explorer-rhs">{this.props.children}</div>
|
||||
</div>
|
||||
<div className="doc-explorer-contents">
|
||||
{shouldSearchBoxAppear && (
|
||||
<SearchBox
|
||||
value={navItem.search}
|
||||
placeholder={`Search ${navItem.name}...`}
|
||||
onSearch={this.handleSearch}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Public API
|
||||
showDoc(typeOrField: GraphQLNamedType | FieldType) {
|
||||
const navStack = this.state.navStack;
|
||||
const topNav = navStack[navStack.length - 1];
|
||||
if (topNav.def !== typeOrField) {
|
||||
this.setState({
|
||||
navStack: navStack.concat([
|
||||
{
|
||||
name: typeOrField.name,
|
||||
def: typeOrField,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
showDocForReference(reference: any) {
|
||||
if (reference && reference.kind === 'Type') {
|
||||
this.showDoc(reference.type);
|
||||
} else if (reference.kind === 'Field') {
|
||||
this.showDoc(reference.field);
|
||||
} else if (reference.kind === 'Argument' && reference.field) {
|
||||
this.showDoc(reference.field);
|
||||
} else if (reference.kind === 'EnumValue' && reference.type) {
|
||||
this.showDoc(reference.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
showSearch(search: string) {
|
||||
const navStack = this.state.navStack.slice();
|
||||
const topNav = navStack[navStack.length - 1];
|
||||
navStack[navStack.length - 1] = { ...topNav, search };
|
||||
this.setState({ navStack });
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({ navStack: [initialNav] });
|
||||
}
|
||||
|
||||
handleNavBackClick = () => {
|
||||
if (this.state.navStack.length > 1) {
|
||||
this.setState({ navStack: this.state.navStack.slice(0, -1) });
|
||||
}
|
||||
};
|
||||
|
||||
handleClickType = (type: GraphQLNamedType) => {
|
||||
this.showDoc(type);
|
||||
};
|
||||
|
||||
handleClickField = (field: FieldType) => {
|
||||
this.showDoc(field);
|
||||
};
|
||||
|
||||
handleSearch = (value: string) => {
|
||||
this.showSearch(value);
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GraphQLArgument } from 'graphql';
|
||||
import TypeLink from './TypeLink';
|
||||
import DefaultValue from './DefaultValue';
|
||||
import { OnClickTypeFunction } from './types';
|
||||
|
||||
type ArgumentProps = {
|
||||
arg: GraphQLArgument;
|
||||
onClickType: OnClickTypeFunction;
|
||||
showDefaultValue?: boolean;
|
||||
};
|
||||
|
||||
export default function Argument({
|
||||
arg,
|
||||
onClickType,
|
||||
showDefaultValue,
|
||||
}: ArgumentProps) {
|
||||
return (
|
||||
<span className="arg">
|
||||
<span className="arg-name">{arg.name}</span>
|
||||
{': '}
|
||||
<TypeLink type={arg.type} onClick={onClickType} />
|
||||
{showDefaultValue !== false && <DefaultValue field={arg} />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { astFromValue, print, ValueNode } from 'graphql';
|
||||
import { FieldType } from './types';
|
||||
|
||||
const printDefault = (ast?: ValueNode | null): string => {
|
||||
if (!ast) {
|
||||
return '';
|
||||
}
|
||||
return print(ast);
|
||||
};
|
||||
|
||||
type DefaultValueProps = {
|
||||
field: FieldType;
|
||||
};
|
||||
|
||||
export default function DefaultValue({ field }: DefaultValueProps) {
|
||||
// field.defaultValue could be null or false, so be careful here!
|
||||
if ('defaultValue' in field && field.defaultValue !== undefined) {
|
||||
return (
|
||||
<span>
|
||||
{' = '}
|
||||
<span className="arg-default-value">
|
||||
{printDefault(astFromValue(field.defaultValue, field.type))}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DirectiveNode } from 'graphql';
|
||||
|
||||
type DirectiveProps = {
|
||||
directive: DirectiveNode;
|
||||
};
|
||||
|
||||
export default function Directive({ directive }: DirectiveProps) {
|
||||
return (
|
||||
<span className="doc-category-item" id={directive.name.value}>
|
||||
{'@'}
|
||||
{directive.name.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Argument from './Argument';
|
||||
import Directive from './Directive';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import TypeLink from './TypeLink';
|
||||
import { GraphQLArgument, DirectiveNode } from 'graphql';
|
||||
import { OnClickTypeFunction, FieldType } from './types';
|
||||
|
||||
type FieldDocProps = {
|
||||
field?: FieldType;
|
||||
onClickType: OnClickTypeFunction;
|
||||
};
|
||||
|
||||
export default function FieldDoc({ field, onClickType }: FieldDocProps) {
|
||||
const [showDeprecated, handleShowDeprecated] = React.useState(false);
|
||||
let argsDef;
|
||||
let deprecatedArgsDef;
|
||||
if (field && 'args' in field && field.args.length > 0) {
|
||||
argsDef = (
|
||||
<div id="doc-args" className="doc-category">
|
||||
<div className="doc-category-title">{'arguments'}</div>
|
||||
{field.args
|
||||
.filter(arg => !arg.deprecationReason)
|
||||
.map((arg: GraphQLArgument) => (
|
||||
<div key={arg.name} className="doc-category-item">
|
||||
<div>
|
||||
<Argument arg={arg} onClickType={onClickType} />
|
||||
</div>
|
||||
<MarkdownContent
|
||||
className="doc-value-description"
|
||||
markdown={arg.description}
|
||||
/>
|
||||
{arg && 'deprecationReason' in arg && (
|
||||
<MarkdownContent
|
||||
className="doc-deprecation"
|
||||
markdown={arg?.deprecationReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const deprecatedArgs = field.args.filter(arg =>
|
||||
Boolean(arg.deprecationReason),
|
||||
);
|
||||
if (deprecatedArgs.length > 0) {
|
||||
deprecatedArgsDef = (
|
||||
<div id="doc-deprecated-args" className="doc-category">
|
||||
<div className="doc-category-title">{'deprecated arguments'}</div>
|
||||
{!showDeprecated ? (
|
||||
<button
|
||||
className="show-btn"
|
||||
onClick={() => handleShowDeprecated(!showDeprecated)}>
|
||||
{'Show deprecated arguments...'}
|
||||
</button>
|
||||
) : (
|
||||
deprecatedArgs.map((arg, i) => (
|
||||
<div key={i}>
|
||||
<div>
|
||||
<Argument arg={arg} onClickType={onClickType} />
|
||||
</div>
|
||||
<MarkdownContent
|
||||
className="doc-value-description"
|
||||
markdown={arg.description}
|
||||
/>
|
||||
{arg && 'deprecationReason' in arg && (
|
||||
<MarkdownContent
|
||||
className="doc-deprecation"
|
||||
markdown={arg?.deprecationReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let directivesDef;
|
||||
if (
|
||||
field &&
|
||||
field.astNode &&
|
||||
field.astNode.directives &&
|
||||
field.astNode.directives.length > 0
|
||||
) {
|
||||
directivesDef = (
|
||||
<div id="doc-directives" className="doc-category">
|
||||
<div className="doc-category-title">{'directives'}</div>
|
||||
{field.astNode.directives.map((directive: DirectiveNode) => (
|
||||
<div key={directive.name.value} className="doc-category-item">
|
||||
<div>
|
||||
<Directive directive={directive} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MarkdownContent
|
||||
className="doc-type-description"
|
||||
markdown={field?.description || 'No Description'}
|
||||
/>
|
||||
{field && 'deprecationReason' in field && (
|
||||
<MarkdownContent
|
||||
className="doc-deprecation"
|
||||
markdown={field?.deprecationReason}
|
||||
/>
|
||||
)}
|
||||
<div className="doc-category">
|
||||
<div className="doc-category-title">{'type'}</div>
|
||||
<TypeLink type={field?.type} onClick={onClickType} />
|
||||
</div>
|
||||
{argsDef}
|
||||
{directivesDef}
|
||||
{deprecatedArgsDef}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MD from 'markdown-it';
|
||||
|
||||
type Maybe<T> = T | null | undefined;
|
||||
|
||||
const md = new MD({
|
||||
// render urls as links, à la github-flavored markdown
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
type MarkdownContentProps = {
|
||||
markdown?: Maybe<string>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function MarkdownContent({
|
||||
markdown,
|
||||
className,
|
||||
}: MarkdownContentProps) {
|
||||
if (!markdown) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: md.render(markdown) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import TypeLink from './TypeLink';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { OnClickTypeFunction } from './types';
|
||||
|
||||
type SchemaDocProps = {
|
||||
schema: GraphQLSchema;
|
||||
onClickType: OnClickTypeFunction;
|
||||
};
|
||||
|
||||
// Render the top level Schema
|
||||
export default function SchemaDoc({ schema, onClickType }: SchemaDocProps) {
|
||||
const queryType = schema.getQueryType();
|
||||
const mutationType = schema.getMutationType && schema.getMutationType();
|
||||
const subscriptionType =
|
||||
schema.getSubscriptionType && schema.getSubscriptionType();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MarkdownContent
|
||||
className="doc-type-description"
|
||||
markdown={
|
||||
schema.description ||
|
||||
'A GraphQL schema provides a root type for each kind of operation.'
|
||||
}
|
||||
/>
|
||||
<div className="doc-category">
|
||||
<div className="doc-category-title">{'root types'}</div>
|
||||
<div className="doc-category-item">
|
||||
<span className="keyword">{'query'}</span>
|
||||
{': '}
|
||||
<TypeLink type={queryType} onClick={onClickType} />
|
||||
</div>
|
||||
{mutationType && (
|
||||
<div className="doc-category-item">
|
||||
<span className="keyword">{'mutation'}</span>
|
||||
{': '}
|
||||
<TypeLink type={mutationType} onClick={onClickType} />
|
||||
</div>
|
||||
)}
|
||||
{subscriptionType && (
|
||||
<div className="doc-category-item">
|
||||
<span className="keyword">{'subscription'}</span>
|
||||
{': '}
|
||||
<TypeLink type={subscriptionType} onClick={onClickType} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { ChangeEventHandler } from 'react';
|
||||
|
||||
import debounce from '../../utility/debounce';
|
||||
|
||||
type OnSearchFn = (value: string) => void;
|
||||
|
||||
type SearchBoxProps = {
|
||||
value?: string;
|
||||
placeholder: string;
|
||||
onSearch: OnSearchFn;
|
||||
};
|
||||
|
||||
type SearchBoxState = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export default class SearchBox extends React.Component<
|
||||
SearchBoxProps,
|
||||
SearchBoxState
|
||||
> {
|
||||
debouncedOnSearch: OnSearchFn;
|
||||
|
||||
constructor(props: SearchBoxProps) {
|
||||
super(props);
|
||||
this.state = { value: props.value || '' };
|
||||
this.debouncedOnSearch = debounce(200, this.props.onSearch);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<label className="search-box">
|
||||
<div className="search-box-icon" aria-hidden="true">
|
||||
{'\u26b2'}
|
||||
</div>
|
||||
<input
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
type="text"
|
||||
placeholder={this.props.placeholder}
|
||||
aria-label={this.props.placeholder}
|
||||
/>
|
||||
{this.state.value && (
|
||||
<button
|
||||
className="search-box-clear"
|
||||
onClick={this.handleClear}
|
||||
aria-label="Clear search input">
|
||||
{'\u2715'}
|
||||
</button>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
handleChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
const value = event.currentTarget.value;
|
||||
this.setState({ value });
|
||||
this.debouncedOnSearch(value);
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.setState({ value: '' });
|
||||
this.props.onSearch('');
|
||||
};
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { GraphQLSchema, GraphQLNamedType } from 'graphql';
|
||||
|
||||
import Argument from './Argument';
|
||||
import TypeLink from './TypeLink';
|
||||
import { OnClickFieldFunction, OnClickTypeFunction } from './types';
|
||||
|
||||
type SearchResultsProps = {
|
||||
schema: GraphQLSchema;
|
||||
withinType?: GraphQLNamedType;
|
||||
searchValue: string;
|
||||
onClickType: OnClickTypeFunction;
|
||||
onClickField: OnClickFieldFunction;
|
||||
};
|
||||
|
||||
export default class SearchResults extends React.Component<
|
||||
SearchResultsProps,
|
||||
{}
|
||||
> {
|
||||
shouldComponentUpdate(nextProps: SearchResultsProps) {
|
||||
return (
|
||||
this.props.schema !== nextProps.schema ||
|
||||
this.props.searchValue !== nextProps.searchValue
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const searchValue = this.props.searchValue;
|
||||
const withinType = this.props.withinType;
|
||||
const schema = this.props.schema;
|
||||
const onClickType = this.props.onClickType;
|
||||
const onClickField = this.props.onClickField;
|
||||
|
||||
const matchedWithin: ReactNode[] = [];
|
||||
const matchedTypes: ReactNode[] = [];
|
||||
const matchedFields: ReactNode[] = [];
|
||||
|
||||
const typeMap = schema.getTypeMap();
|
||||
let typeNames = Object.keys(typeMap);
|
||||
|
||||
// Move the within type name to be the first searched.
|
||||
if (withinType) {
|
||||
typeNames = typeNames.filter(n => n !== withinType.name);
|
||||
typeNames.unshift(withinType.name);
|
||||
}
|
||||
|
||||
for (const typeName of typeNames) {
|
||||
if (
|
||||
matchedWithin.length + matchedTypes.length + matchedFields.length >=
|
||||
100
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const type = typeMap[typeName];
|
||||
if (withinType !== type && isMatch(typeName, searchValue)) {
|
||||
matchedTypes.push(
|
||||
<div className="doc-category-item" key={typeName}>
|
||||
<TypeLink type={type} onClick={onClickType} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (type && 'getFields' in type) {
|
||||
const fields = type.getFields();
|
||||
Object.keys(fields).forEach(fieldName => {
|
||||
const field = fields[fieldName];
|
||||
let matchingArgs;
|
||||
|
||||
if (!isMatch(fieldName, searchValue)) {
|
||||
if ('args' in field && field.args.length) {
|
||||
matchingArgs = field.args.filter(arg =>
|
||||
isMatch(arg.name, searchValue),
|
||||
);
|
||||
if (matchingArgs.length === 0) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const match = (
|
||||
<div className="doc-category-item" key={typeName + '.' + fieldName}>
|
||||
{withinType !== type && [
|
||||
<TypeLink key="type" type={type} onClick={onClickType} />,
|
||||
'.',
|
||||
]}
|
||||
<a
|
||||
className="field-name"
|
||||
onClick={event => onClickField(field, type, event)}>
|
||||
{field.name}
|
||||
</a>
|
||||
{matchingArgs && [
|
||||
'(',
|
||||
<span key="args">
|
||||
{matchingArgs.map(arg => (
|
||||
<Argument
|
||||
key={arg.name}
|
||||
arg={arg}
|
||||
onClickType={onClickType}
|
||||
showDefaultValue={false}
|
||||
/>
|
||||
))}
|
||||
</span>,
|
||||
')',
|
||||
]}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (withinType === type) {
|
||||
matchedWithin.push(match);
|
||||
} else {
|
||||
matchedFields.push(match);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
matchedWithin.length + matchedTypes.length + matchedFields.length ===
|
||||
0
|
||||
) {
|
||||
return <span className="doc-alert-text">{'No results found.'}</span>;
|
||||
}
|
||||
|
||||
if (withinType && matchedTypes.length + matchedFields.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
{matchedWithin}
|
||||
<div className="doc-category">
|
||||
<div className="doc-category-title">{'other results'}</div>
|
||||
{matchedTypes}
|
||||
{matchedFields}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="doc-search-items">
|
||||
{matchedWithin}
|
||||
{matchedTypes}
|
||||
{matchedFields}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isMatch(sourceText: string, searchValue: string) {
|
||||
try {
|
||||
const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch);
|
||||
return sourceText.search(new RegExp(escaped, 'i')) !== -1;
|
||||
} catch (e) {
|
||||
return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1;
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
GraphQLSchema,
|
||||
GraphQLObjectType,
|
||||
GraphQLInterfaceType,
|
||||
GraphQLUnionType,
|
||||
GraphQLEnumType,
|
||||
GraphQLType,
|
||||
GraphQLEnumValue,
|
||||
} from 'graphql';
|
||||
|
||||
import Argument from './Argument';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import TypeLink from './TypeLink';
|
||||
import DefaultValue from './DefaultValue';
|
||||
import { FieldType, OnClickTypeFunction, OnClickFieldFunction } from './types';
|
||||
|
||||
type TypeDocProps = {
|
||||
schema: GraphQLSchema;
|
||||
type: GraphQLType;
|
||||
onClickType: OnClickTypeFunction;
|
||||
onClickField: OnClickFieldFunction;
|
||||
};
|
||||
|
||||
type TypeDocState = {
|
||||
showDeprecated: boolean;
|
||||
};
|
||||
|
||||
export default class TypeDoc extends React.Component<
|
||||
TypeDocProps,
|
||||
TypeDocState
|
||||
> {
|
||||
constructor(props: TypeDocProps) {
|
||||
super(props);
|
||||
this.state = { showDeprecated: false };
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: TypeDocProps, nextState: TypeDocState) {
|
||||
return (
|
||||
this.props.type !== nextProps.type ||
|
||||
this.props.schema !== nextProps.schema ||
|
||||
this.state.showDeprecated !== nextState.showDeprecated
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const schema = this.props.schema;
|
||||
const type = this.props.type;
|
||||
const onClickType = this.props.onClickType;
|
||||
const onClickField = this.props.onClickField;
|
||||
|
||||
let typesTitle: string | null = null;
|
||||
let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = [];
|
||||
if (type instanceof GraphQLUnionType) {
|
||||
typesTitle = 'possible types';
|
||||
types = schema.getPossibleTypes(type);
|
||||
} else if (type instanceof GraphQLInterfaceType) {
|
||||
typesTitle = 'implementations';
|
||||
types = schema.getPossibleTypes(type);
|
||||
} else if (type instanceof GraphQLObjectType) {
|
||||
typesTitle = 'implements';
|
||||
types = type.getInterfaces();
|
||||
}
|
||||
|
||||
let typesDef;
|
||||
if (types && types.length > 0) {
|
||||
typesDef = (
|
||||
<div id="doc-types" className="doc-category">
|
||||
<div className="doc-category-title">{typesTitle}</div>
|
||||
{types.map(subtype => (
|
||||
<div key={subtype.name} className="doc-category-item">
|
||||
<TypeLink type={subtype} onClick={onClickType} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// InputObject and Object
|
||||
let fieldsDef;
|
||||
let deprecatedFieldsDef;
|
||||
if (type && 'getFields' in type) {
|
||||
const fieldMap = type.getFields();
|
||||
const fields = Object.keys(fieldMap).map(name => fieldMap[name]);
|
||||
fieldsDef = (
|
||||
<div id="doc-fields" className="doc-category">
|
||||
<div className="doc-category-title">{'fields'}</div>
|
||||
{fields
|
||||
.filter(field => !field.deprecationReason)
|
||||
.map(field => (
|
||||
<Field
|
||||
key={field.name}
|
||||
type={type}
|
||||
field={field}
|
||||
onClickType={onClickType}
|
||||
onClickField={onClickField}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const deprecatedFields = fields.filter(field =>
|
||||
Boolean(field.deprecationReason),
|
||||
);
|
||||
if (deprecatedFields.length > 0) {
|
||||
deprecatedFieldsDef = (
|
||||
<div id="doc-deprecated-fields" className="doc-category">
|
||||
<div className="doc-category-title">{'deprecated fields'}</div>
|
||||
{!this.state.showDeprecated ? (
|
||||
<button className="show-btn" onClick={this.handleShowDeprecated}>
|
||||
{'Show deprecated fields...'}
|
||||
</button>
|
||||
) : (
|
||||
deprecatedFields.map(field => (
|
||||
<Field
|
||||
key={field.name}
|
||||
type={type}
|
||||
field={field}
|
||||
onClickType={onClickType}
|
||||
onClickField={onClickField}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let valuesDef: ReactNode;
|
||||
let deprecatedValuesDef: ReactNode;
|
||||
if (type instanceof GraphQLEnumType) {
|
||||
const values = type.getValues();
|
||||
valuesDef = (
|
||||
<div className="doc-category">
|
||||
<div className="doc-category-title">{'values'}</div>
|
||||
{values
|
||||
.filter(value => Boolean(!value.deprecationReason))
|
||||
.map(value => (
|
||||
<EnumValue key={value.name} value={value} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const deprecatedValues = values.filter(value =>
|
||||
Boolean(value.deprecationReason),
|
||||
);
|
||||
if (deprecatedValues.length > 0) {
|
||||
deprecatedValuesDef = (
|
||||
<div className="doc-category">
|
||||
<div className="doc-category-title">{'deprecated values'}</div>
|
||||
{!this.state.showDeprecated ? (
|
||||
<button className="show-btn" onClick={this.handleShowDeprecated}>
|
||||
{'Show deprecated values...'}
|
||||
</button>
|
||||
) : (
|
||||
deprecatedValues.map(value => (
|
||||
<EnumValue key={value.name} value={value} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MarkdownContent
|
||||
className="doc-type-description"
|
||||
markdown={
|
||||
('description' in type && type.description) || 'No Description'
|
||||
}
|
||||
/>
|
||||
{type instanceof GraphQLObjectType && typesDef}
|
||||
{fieldsDef}
|
||||
{deprecatedFieldsDef}
|
||||
{valuesDef}
|
||||
{deprecatedValuesDef}
|
||||
{!(type instanceof GraphQLObjectType) && typesDef}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleShowDeprecated = () => this.setState({ showDeprecated: true });
|
||||
}
|
||||
|
||||
type FieldProps = {
|
||||
type: GraphQLType;
|
||||
field: FieldType;
|
||||
onClickType: OnClickTypeFunction;
|
||||
onClickField: OnClickFieldFunction;
|
||||
};
|
||||
|
||||
function Field({ type, field, onClickType, onClickField }: FieldProps) {
|
||||
return (
|
||||
<div className="doc-category-item">
|
||||
<a
|
||||
className="field-name"
|
||||
onClick={event => onClickField(field, type, event)}>
|
||||
{field.name}
|
||||
</a>
|
||||
{'args' in field &&
|
||||
field.args &&
|
||||
field.args.length > 0 && [
|
||||
'(',
|
||||
<span key="args">
|
||||
{field.args
|
||||
.filter(arg => !arg.deprecationReason)
|
||||
.map(arg => (
|
||||
<Argument key={arg.name} arg={arg} onClickType={onClickType} />
|
||||
))}
|
||||
</span>,
|
||||
')',
|
||||
]}
|
||||
{': '}
|
||||
<TypeLink type={field.type} onClick={onClickType} />
|
||||
<DefaultValue field={field} />
|
||||
{field.description && (
|
||||
<MarkdownContent
|
||||
className="field-short-description"
|
||||
markdown={field.description}
|
||||
/>
|
||||
)}
|
||||
{'deprecationReason' in field && field.deprecationReason && (
|
||||
<MarkdownContent
|
||||
className="doc-deprecation"
|
||||
markdown={field.deprecationReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type EnumValue = {
|
||||
value: GraphQLEnumValue;
|
||||
};
|
||||
|
||||
function EnumValue({ value }: EnumValue) {
|
||||
return (
|
||||
<div className="doc-category-item">
|
||||
<div className="enum-value">{value.name}</div>
|
||||
<MarkdownContent
|
||||
className="doc-value-description"
|
||||
markdown={value.description}
|
||||
/>
|
||||
{value.deprecationReason && (
|
||||
<MarkdownContent
|
||||
className="doc-deprecation"
|
||||
markdown={value.deprecationReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLType,
|
||||
GraphQLNamedType,
|
||||
} from 'graphql';
|
||||
import { OnClickTypeFunction } from './types';
|
||||
|
||||
type Maybe<T> = T | null | undefined;
|
||||
|
||||
type TypeLinkProps = {
|
||||
type?: Maybe<GraphQLType>;
|
||||
onClick?: OnClickTypeFunction;
|
||||
};
|
||||
|
||||
export default function TypeLink(props: TypeLinkProps) {
|
||||
const onClick = props.onClick ? props.onClick : () => null;
|
||||
return renderType(props.type, onClick);
|
||||
}
|
||||
|
||||
function renderType(type: Maybe<GraphQLType>, onClick: OnClickTypeFunction) {
|
||||
if (type instanceof GraphQLNonNull) {
|
||||
return (
|
||||
<span>
|
||||
{renderType(type.ofType, onClick)}
|
||||
{'!'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (type instanceof GraphQLList) {
|
||||
return (
|
||||
<span>
|
||||
{'['}
|
||||
{renderType(type.ofType, onClick)}
|
||||
{']'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
className="type-name"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
onClick(type as GraphQLNamedType, event);
|
||||
}}
|
||||
href="#">
|
||||
{type?.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import {
|
||||
GraphQLField,
|
||||
GraphQLInputField,
|
||||
GraphQLArgument,
|
||||
GraphQLObjectType,
|
||||
GraphQLInterfaceType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLType,
|
||||
GraphQLNamedType,
|
||||
} from 'graphql';
|
||||
|
||||
export type FieldType =
|
||||
| GraphQLField<{}, {}, {}>
|
||||
| GraphQLInputField
|
||||
| GraphQLArgument;
|
||||
|
||||
export type OnClickFieldFunction = (
|
||||
field: FieldType,
|
||||
type?:
|
||||
| GraphQLObjectType
|
||||
| GraphQLInterfaceType
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLType,
|
||||
event?: MouseEvent,
|
||||
) => void;
|
||||
|
||||
export type OnClickTypeFunction = (
|
||||
type: GraphQLNamedType,
|
||||
event?: MouseEvent<HTMLAnchorElement>,
|
||||
) => void;
|
||||
|
||||
export type OnClickFieldOrTypeFunction =
|
||||
| OnClickFieldFunction
|
||||
| OnClickTypeFunction;
|
||||
@@ -1,297 +0,0 @@
|
||||
.graphql-docs-container .doc-explorer {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-title-bar,
|
||||
.graphql-docs-container .history-title-bar {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
line-height: 14px;
|
||||
padding: 8px 8px 5px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-title,
|
||||
.graphql-docs-container .history-title {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
overflow-x: hidden;
|
||||
padding: 10px 0 10px 10px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-back {
|
||||
color: #3B5998;
|
||||
cursor: pointer;
|
||||
margin: -7px 0 -6px -8px;
|
||||
overflow-x: hidden;
|
||||
padding: 17px 12px 16px 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background: 0;
|
||||
border: 0;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.doc-explorer-narrow .doc-explorer-back {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-back:before {
|
||||
border-left: 2px solid #3B5998;
|
||||
border-top: 2px solid #3B5998;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 9px;
|
||||
margin: 0 3px -1px 0;
|
||||
position: relative;
|
||||
transform: rotate(-45deg);
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-rhs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-contents,
|
||||
.graphql-docs-container .history-contents {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #d6d6d6;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
padding: 20px 15px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 47px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-contents {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-type-description p:first-child ,
|
||||
.graphql-docs-container .doc-type-description blockquote:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-contents a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-explorer-contents a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-value-description > :first-child {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-value-description > :last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-type-description code,
|
||||
.graphql-docs-container .doc-type-description pre,
|
||||
.graphql-docs-container .doc-category code,
|
||||
.graphql-docs-container .doc-category pre {
|
||||
--saf-0: rgba(var(--sk_foreground_low,29,28,29),0.13);
|
||||
font-size: 12px;
|
||||
line-height: 1.50001;
|
||||
font-variant-ligatures: none;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
-webkit-tab-size: 4;
|
||||
-moz-tab-size: 4;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-type-description code,
|
||||
.graphql-docs-container .doc-category code {
|
||||
padding: 2px 3px 1px;
|
||||
border: 1px solid var(--saf-0);
|
||||
border-radius: 3px;
|
||||
background-color: rgba(var(--sk_foreground_min,29,28,29),.04);
|
||||
color: #e01e5a;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-category {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-category-title {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
color: #777;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
font-variant: small-caps;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
margin: 0 -15px 10px 0;
|
||||
padding: 10px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-category-item {
|
||||
margin: 12px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.graphql-docs-container .keyword {
|
||||
color: #B11A04;
|
||||
}
|
||||
|
||||
.graphql-docs-container .type-name {
|
||||
color: #CA9800;
|
||||
}
|
||||
|
||||
.graphql-docs-container .field-name {
|
||||
color: #1F61A0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .field-short-description {
|
||||
color: #666;
|
||||
margin-left: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.graphql-docs-container .enum-value {
|
||||
color: #0B7FC7;
|
||||
}
|
||||
|
||||
.graphql-docs-container .arg-name {
|
||||
color: #8B2BB9;
|
||||
}
|
||||
|
||||
.graphql-docs-container .arg {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.graphql-docs-container .arg:first-child:last-child,
|
||||
.graphql-docs-container .arg:first-child:nth-last-child(2),
|
||||
.graphql-docs-container .arg:first-child:nth-last-child(2) ~ .arg {
|
||||
display: inherit;
|
||||
margin: inherit;
|
||||
}
|
||||
|
||||
.graphql-docs-container .arg:first-child:nth-last-child(2):after {
|
||||
content: ', ';
|
||||
}
|
||||
|
||||
.graphql-docs-container .arg-default-value {
|
||||
color: #43A047;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-deprecation {
|
||||
background: #fffae8;
|
||||
box-shadow: inset 0 0 1px #bfb063;
|
||||
color: #867F70;
|
||||
line-height: 16px;
|
||||
margin: 8px -8px;
|
||||
max-height: 80px;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-deprecation:before {
|
||||
content: 'Deprecated:';
|
||||
color: #c79b2e;
|
||||
cursor: default;
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
padding-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-deprecation > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .doc-deprecation > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .show-btn {
|
||||
-webkit-appearance: initial;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #ccc;
|
||||
text-align: center;
|
||||
padding: 8px 12px 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #fbfcfc;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.graphql-docs-container .search-box {
|
||||
border-bottom: 1px solid #d3d6db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin: -15px -15px 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graphql-docs-container .search-box-icon {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
transform: rotate(-45deg);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.graphql-docs-container .search-box .search-box-clear {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding: 1px 5px 2px;
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
user-select: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.graphql-docs-container .search-box .search-box-clear:hover {
|
||||
background-color: #b9b9b9;
|
||||
}
|
||||
|
||||
.graphql-docs-container .search-box > input {
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
padding: 6px 24px 8px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.graphql-docs-container .error-container {
|
||||
font-weight: bold;
|
||||
left: 0;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DocExplorer } from "./components/DocExplorer";
|
||||
|
||||
// Todo: Rollup throws error
|
||||
import './index.css';
|
||||
|
||||
export {
|
||||
DocExplorer
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provided a duration and a function, returns a new function which is called
|
||||
* `duration` milliseconds after the last call.
|
||||
*/
|
||||
export default function debounce<F extends (...args: any[]) => any>(
|
||||
duration: number,
|
||||
fn: F,
|
||||
) {
|
||||
let timeout: number | null;
|
||||
return function (this: any, ...args: Parameters<F>) {
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = null;
|
||||
fn.apply(this, args);
|
||||
}, duration);
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"declarationDir": "types",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"src/**/*.test.tsx"
|
||||
],
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
The schema definition for collections
|
||||
|
||||
### Publish to Npm Registry
|
||||
```bash
|
||||
npm publish --access=public
|
||||
```
|
||||
|
||||
### Collection schema
|
||||
```bash
|
||||
id Unique id (when persisted to a db)
|
||||
|
||||
@@ -29,19 +29,13 @@ const keyValueSchema = Yup.object({
|
||||
const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined();
|
||||
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
|
||||
|
||||
const graphqlBodySchema = Yup.object({
|
||||
query: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||
variables: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
const requestBodySchema = Yup.object({
|
||||
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql']).required('mode is required'),
|
||||
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'),
|
||||
json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||
text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||
xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
|
||||
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
||||
multipartForm: Yup.array().of(keyValueSchema).nullable(),
|
||||
graphql: graphqlBodySchema.nullable(),
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
// Right now, the request schema is very tightly coupled with http request
|
||||
|
||||
@@ -7,7 +7,6 @@ const { devices } = require('@playwright/test');
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
process.env.PLAYWRIGHT = "1";
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
|
||||
@@ -48,8 +48,8 @@ Woof! If you like project, hit that ⭐ button !!
|
||||
|
||||
### Stay in touch 🌐
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
[Website](https://www.usebruno.com)
|
||||
|
||||
|
||||
### License 📄
|
||||
[MIT](license.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { faker } = require('@faker-js/faker');
|
||||
const { HomePage } = require('../tests/pages/home.page');
|
||||
import * as faker from './utils/data-faker';
|
||||
|
||||
test.describe('bruno e2e test', () => {
|
||||
let homePage;
|
||||
@@ -13,22 +13,9 @@ test.describe('bruno e2e test', () => {
|
||||
await expect(page).toHaveTitle(/bruno/);
|
||||
});
|
||||
|
||||
test('user should be able to create new collection & new request', async () => {
|
||||
await homePage.createNewCollection(faker.randomWords);
|
||||
await expect(homePage.createNewCollectionSuccessToast).toBeVisible();
|
||||
|
||||
// using fake data to simulate negative case
|
||||
await homePage.createNewRequest(faker.randomVerb, faker.randomHttpMethod, faker.randomUrl);
|
||||
await expect(homePage.networkErrorToast).toBeVisible();
|
||||
|
||||
// using real data to simulate positive case
|
||||
await homePage.createNewRequest('Single User', 'GET', 'https://reqres.in/api/users/2');
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
});
|
||||
|
||||
test('user should be able to load & use sample collection', async () => {
|
||||
await homePage.loadSampleCollection();
|
||||
await expect(homePage.loadSampleCollectionSuccessToast).toBeVisible();
|
||||
await expect(homePage.loadSampleCollectionToastSuccess).toBeVisible();
|
||||
|
||||
await homePage.getUsers();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
@@ -46,4 +33,9 @@ test.describe('bruno e2e test', () => {
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
});
|
||||
|
||||
test('user should be able to create new collection', async () => {
|
||||
await homePage.createCollection(faker.random.words());
|
||||
await expect(homePage.createCollectionToastSuccess).toBeVisible();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ exports.HomePage = class HomePage {
|
||||
this.loadSampleCollectionSelector = page.locator('#load-sample-collection');
|
||||
|
||||
// sample collection
|
||||
this.loadSampleCollectionSuccessToast = page.getByText('Sample Collection loaded successfully');
|
||||
this.loadSampleCollectionToastSuccess = page.getByText('Sample Collection loaded successfully');
|
||||
this.sampeCollectionSelector = page.locator('#sidebar-collection-name');
|
||||
this.getUsersSelector = page.getByText('Users');
|
||||
this.getSingleUserSelector = page.getByText('Single User');
|
||||
@@ -26,12 +26,7 @@ exports.HomePage = class HomePage {
|
||||
// create collection
|
||||
this.collectionNameField = page.locator('#collection-name');
|
||||
this.submitButton = page.locator(`button[type='submit']`);
|
||||
this.createNewCollectionSuccessToast = page.getByText('Collection created');
|
||||
this.createNewTab = page.locator('#create-new-tab');
|
||||
this.requestNameField = page.locator('input[name="requestName"]');
|
||||
this.methodName = page.locator('#create-new-request-method').first();
|
||||
this.requestUrlField = page.locator('#request-url');
|
||||
this.networkErrorToast = page.getByText('Network Error');
|
||||
this.createCollectionToastSuccess = page.getByText('Collection created');
|
||||
}
|
||||
|
||||
async open() {
|
||||
@@ -68,19 +63,9 @@ exports.HomePage = class HomePage {
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async createNewCollection(collectionName) {
|
||||
async createCollection(collectionName) {
|
||||
await this.createCollectionSelector.click();
|
||||
await this.collectionNameField.fill(collectionName);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async createNewRequest(name, method, endpoint) {
|
||||
await this.createNewTab.click();
|
||||
await this.requestNameField.fill(name);
|
||||
await this.methodName.click();
|
||||
await this.page.click(`text=${method}`);
|
||||
await this.requestUrlField.fill(endpoint);
|
||||
await this.submitButton.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
const { faker } = require('@faker-js/faker');
|
||||
|
||||
export let randomWords = faker.random.words();
|
||||
export let randomVerb = faker.hacker.verb();
|
||||
export let randomHttpMethod = faker.internet.httpMethod();
|
||||
export let randomUrl = faker.internet.url();
|
||||
Reference in New Issue
Block a user