feat(#206): Collection and Env variables viewer

This commit is contained in:
Anoop M D 2023-09-24 02:07:31 +05:30
parent 1c89ab3450
commit 0a172ddce8
15 changed files with 185 additions and 199 deletions

View File

@ -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",

View File

@ -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} />;

View File

@ -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>

View File

@ -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;

View File

@ -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) {

View File

@ -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>
);
})

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 {