mirror of
https://github.com/usebruno/bruno.git
synced 2025-02-22 20:51:23 +01:00
feat(#206): Collection and Env variables viewer
This commit is contained in:
parent
1c89ab3450
commit
0a172ddce8
@ -47,6 +47,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
|
@ -13,6 +13,7 @@ import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import VariablesEditor from 'components/VariablesEditor';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -123,6 +124,10 @@ const RequestTabPanel = () => {
|
||||
return <RunnerResults collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'variables') {
|
||||
return <VariablesEditor collection={collection} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { IconFiles, IconRun } from '@tabler/icons';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconFiles, IconRun, IconEye } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import VariablesView from 'components/VariablesView';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -17,6 +18,16 @@ const CollectionToolBar = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const viewVariables = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'variables'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
@ -28,7 +39,9 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
<VariablesView collection={collection} />
|
||||
<span className="mr-3">
|
||||
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
|
||||
</span>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconVariable } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, text }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">{text}</span>
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialTab;
|
@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -56,6 +57,14 @@ const RequestTab = ({ tab, collection }) => {
|
||||
return color;
|
||||
};
|
||||
|
||||
if (tab.type === 'variables') {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, tab.uid);
|
||||
|
||||
if (!item) {
|
||||
|
@ -114,7 +114,7 @@ const RequestTabs = () => {
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
li {
|
||||
background-color: ${(props) => props.theme.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
92
packages/bruno-app/src/components/VariablesEditor/index.js
Normal file
92
packages/bruno-app/src/components/VariablesEditor/index.js
Normal file
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { Inspector } from 'react-inspector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const KeyValueExplorer = ({ data, theme }) => {
|
||||
data = data || {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="border-collapse">
|
||||
<tbody>
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td className="px-2 py-1">{key}</td>
|
||||
<td className="px-2 py-1">
|
||||
<Inspector data={value} theme={theme} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvVariables = ({ collection, theme }) => {
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
if (!environment) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
|
||||
<div className="muted">No environment selected</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const envVars = get(environment, 'variables', []);
|
||||
const enabledEnvVars = filter(envVars, (variable) => variable.enabled);
|
||||
const envVarsObj = enabledEnvVars.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center mt-4 mb-2">
|
||||
<h1 className="font-semibold">Environment Variables</h1>
|
||||
<span className="muted ml-2">({environment.name})</span>
|
||||
</div>
|
||||
{enabledEnvVars.length > 0 ? (
|
||||
<KeyValueExplorer data={envVarsObj} theme={theme} />
|
||||
) : (
|
||||
<div className="muted">No environment variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionVariables = ({ collection, theme }) => {
|
||||
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mb-2">Collection Variables</h1>
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
|
||||
) : (
|
||||
<div className="muted">No collection variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const VariablesEditor = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesEditor;
|
@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
min-width: fit-content;
|
||||
font-size: 14px;
|
||||
top: 36px;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
background-color: ${(props) => props.theme.variables.bg};
|
||||
|
||||
.popover {
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -1,26 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useOnClickOutside from 'hooks/useOnClickOutside';
|
||||
|
||||
const PopOver = ({ children, iconRef, handleClose }) => {
|
||||
const popOverRef = useRef(null);
|
||||
|
||||
useOnClickOutside(popOverRef, (e) => {
|
||||
if (iconRef && iconRef.current) {
|
||||
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="popover" ref={popOverRef}>
|
||||
<div className="popover-content">{children}</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopOver;
|
@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.view-environment {
|
||||
width: 1rem;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.variable-name {
|
||||
color: ${(props) => props.theme.variables.name.color};
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
max-width: 600px;
|
||||
inline-size: 600px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import isObject from 'lodash/isObject';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
const collectionVars = [];
|
||||
|
||||
forOwn(cloneDeep(collectionVariables), (value, key) => {
|
||||
collectionVars.push({
|
||||
uid: uuid(),
|
||||
name: key,
|
||||
value: value
|
||||
});
|
||||
});
|
||||
|
||||
const getValueToDisplay = (value) => {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return isObject(value) ? JSON.stringify(value) : value;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="mb-2 font-medium">Environment Variables</div>
|
||||
{variables && variables.length ? (
|
||||
variables.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
|
||||
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">
|
||||
{getValueToDisplay(variable.value)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<small>No env variables found</small>
|
||||
)}
|
||||
|
||||
<div className="mt-2 font-medium">Collection Variables</div>
|
||||
{collectionVars && collectionVars.length ? (
|
||||
collectionVars.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
|
||||
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">
|
||||
{getValueToDisplay(variable.value)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<small>No collection variables found</small>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesTable;
|
@ -1,48 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import VariablesTable from './VariablesTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import PopOver from './Popover';
|
||||
import { IconEye } from '@tabler/icons';
|
||||
|
||||
const VariablesView = ({ collection }) => {
|
||||
const iconRef = useRef(null);
|
||||
const [popOverOpen, setPopOverOpen] = useState(false);
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const variables = get(environment, 'variables', []);
|
||||
const enabledVariables = filter(variables, (variable) => variable.enabled);
|
||||
const showVariablesTable =
|
||||
enabledVariables.length > 0 ||
|
||||
(collection.collectionVariables && Object.keys(collection.collectionVariables).length > 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mr-2 server-syncstatus-icon" ref={iconRef}>
|
||||
<div
|
||||
className="flex p-1 items-center"
|
||||
onClick={() => setPopOverOpen(true)}
|
||||
onMouseEnter={() => setPopOverOpen(true)}
|
||||
onMouseLeave={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className="cursor-pointer view-environment">
|
||||
<IconEye size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
{popOverOpen && (
|
||||
<PopOver iconRef={iconRef} handleClose={() => setPopOverOpen(false)}>
|
||||
<div className="px-2 py-1">
|
||||
{showVariablesTable ? (
|
||||
<VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables} />
|
||||
) : (
|
||||
'No variables found'
|
||||
)}
|
||||
</div>
|
||||
</PopOver>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesView;
|
@ -19,12 +19,22 @@ export const tabsSlice = createSlice({
|
||||
if (alreadyExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.payload.type === 'variables') {
|
||||
const tab = find(state.tabs, (t) => t.collectionUid === action.payload.collectionUid && t.type === 'variables');
|
||||
if (tab) {
|
||||
state.activeTabUid = tab.uid;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.tabs.push({
|
||||
uid: action.payload.uid,
|
||||
collectionUid: action.payload.collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response'
|
||||
responsePaneTab: 'response',
|
||||
type: action.payload.type || 'request'
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
@ -55,16 +65,22 @@ export const tabsSlice = createSlice({
|
||||
closeTabs: (state, action) => {
|
||||
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
const tabUids = action.payload.tabUids || [];
|
||||
|
||||
// remove the tabs from the state
|
||||
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
|
||||
|
||||
if (activeTab && state.tabs.length) {
|
||||
const { collectionUid } = activeTab;
|
||||
const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
|
||||
// if the active tab no longer exists, set the active tab to the last tab in the list
|
||||
// this implies that the active tab was closed
|
||||
if (!activeTabStillExists) {
|
||||
// attempt to load sibling tabs (based on collections) of the dead tab
|
||||
// load sibling tabs of the current collection
|
||||
const siblingTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);
|
||||
|
||||
// if there are sibling tabs, set the active tab to the last sibling tab
|
||||
// otherwise, set the active tab to the last tab in the list
|
||||
if (siblingTabs && siblingTabs.length) {
|
||||
state.activeTabUid = last(siblingTabs).uid;
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user