mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-22 07:53:34 +01:00
feat: environment variables grid
This commit is contained in:
parent
6a36313e0e
commit
2efc11ff6b
@ -26,7 +26,7 @@
|
|||||||
"graphql": "^16.2.0",
|
"graphql": "^16.2.0",
|
||||||
"graphql-request": "^3.7.0",
|
"graphql-request": "^3.7.0",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.12",
|
"immer": "^9.0.15",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
thead, td {
|
||||||
|
border: 1px solid #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
color: #616161;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
outline: none !important;
|
||||||
|
|
||||||
|
&:focus{
|
||||||
|
outline: none !important;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,127 @@
|
|||||||
|
import React, { useReducer } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import { IconTrash } from '@tabler/icons';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import reducer from './reducer';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const EnvironmentVariables = ({environment, collection}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [state, reducerDispatch] = useReducer(reducer, {hasChanges: false, variables: environment.variables || []});
|
||||||
|
const {
|
||||||
|
variables,
|
||||||
|
hasChanges
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid))
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Changes saved successfully");
|
||||||
|
reducerDispatch({
|
||||||
|
type: 'CHANGES_SAVED'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => toast.error("An error occured while saving the changes"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVariable = () => {
|
||||||
|
reducerDispatch({
|
||||||
|
type: 'ADD_VAR'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVarChange = (e, _variable, type) => {
|
||||||
|
const variable = cloneDeep(_variable);
|
||||||
|
switch(type) {
|
||||||
|
case 'name' : {
|
||||||
|
variable.name = e.target.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'value' : {
|
||||||
|
variable.value = e.target.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'enabled' : {
|
||||||
|
variable.enabled = e.target.checked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reducerDispatch({
|
||||||
|
type: 'UPDATE_VAR',
|
||||||
|
variable
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveVars = (variable) => {
|
||||||
|
reducerDispatch({
|
||||||
|
type: 'DELETE_VAR',
|
||||||
|
variable
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="w-full mt-6 mb-6">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Value</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{variables && variables.length ? variables.map((variable, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={variable.uid}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
|
||||||
|
value={variable.name}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleVarChange(e, variable, 'name')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
|
||||||
|
value={variable.value}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleVarChange(e, variable, 'value')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={variable.enabled}
|
||||||
|
className="mr-3 mousetrap"
|
||||||
|
onChange={(e) => handleVarChange(e, variable, 'enabled')}
|
||||||
|
/>
|
||||||
|
<button onClick={() => handleRemoveVars(variable)}>
|
||||||
|
<IconTrash strokeWidth={1.5} size={20}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>+ Add Variable</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" className="submit btn btn-md btn-secondary mt-2" disabled={!hasChanges} onClick={saveChanges}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
export default EnvironmentVariables;
|
@ -0,0 +1,50 @@
|
|||||||
|
import produce from 'immer';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
import filter from 'lodash/filter';
|
||||||
|
import { uuid } from 'utils/common';
|
||||||
|
|
||||||
|
const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_VAR': {
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
draft.variables.push({
|
||||||
|
uid: uuid(),
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
type: 'text',
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
draft.hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_VAR': {
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
const variable = find(draft.variables, (v) => v.uid === action.variable.uid);
|
||||||
|
variable.name = action.variable.name;
|
||||||
|
variable.value = action.variable.value;
|
||||||
|
variable.enabled = action.variable.enabled;
|
||||||
|
draft.hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'DELETE_VAR': {
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
draft.variables = filter(draft.variables, (v) => v.uid !== action.variable.uid);
|
||||||
|
draft.hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CHANGES_SAVED': {
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
draft.hasChanges = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reducer;
|
@ -1,23 +1,32 @@
|
|||||||
import React, {useState } from "react";
|
import React, {useState } from "react";
|
||||||
import { IconEdit, IconTrash } from "@tabler/icons";
|
import { IconEdit, IconTrash, IconDatabase } from "@tabler/icons";
|
||||||
|
import EnvironmentVariables from './EnvironmentVariables';
|
||||||
import RenameEnvironment from "../../RenameEnvironment";
|
import RenameEnvironment from "../../RenameEnvironment";
|
||||||
import DeleteEnvironment from "../../DeleteEnvironment";
|
import DeleteEnvironment from "../../DeleteEnvironment";
|
||||||
|
|
||||||
const EnvironmentDetails = ({environment, collection}) => {
|
const EnvironmentDetails = ({environment, collection}) => {
|
||||||
const [ openEditModal, setOpenEditModal] = useState(false);
|
const [ openEditModal, setOpenEditModal] = useState(false);
|
||||||
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
|
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||||
|
console.log(environment);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-6 flex-grow flex pt-6" style={{maxWidth: '700px'}}>
|
<div className="px-6 flex-grow flex flex-col pt-6" style={{maxWidth: '700px'}}>
|
||||||
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
|
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
|
||||||
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
|
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
|
||||||
<div className="flex flex-grow">
|
<div className="flex">
|
||||||
<div className="flex-grow font-medium">{environment.name}</div>
|
<div className="flex flex-grow items-center">
|
||||||
<div className="flex gap-x-4 pl-4 pr-6">
|
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5}/>
|
||||||
|
<span className="ml-1 font-semibold">{environment.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-4 pl-4">
|
||||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
|
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
|
||||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
|
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<EnvironmentVariables key={environment.uid} environment={environment} collection={collection}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
addEnvironment as _addEnvironment,
|
addEnvironment as _addEnvironment,
|
||||||
renameEnvironment as _renameEnvironment,
|
renameEnvironment as _renameEnvironment,
|
||||||
deleteEnvironment as _deleteEnvironment,
|
deleteEnvironment as _deleteEnvironment,
|
||||||
|
saveEnvironment as _saveEnvironment,
|
||||||
createCollection as _createCollection,
|
createCollection as _createCollection,
|
||||||
renameCollection as _renameCollection,
|
renameCollection as _renameCollection,
|
||||||
deleteCollection as _deleteCollection,
|
deleteCollection as _deleteCollection,
|
||||||
@ -691,6 +692,33 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch,
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const saveEnvironment = (variables, environmentUid, collectionUid) => (dispatch, getState) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const state = getState();
|
||||||
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
if(!collection) {
|
||||||
|
return reject(new Error('Collection not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionCopy = cloneDeep(collection);
|
||||||
|
const environment = findEnvironmentInCollection(collectionCopy, environmentUid);
|
||||||
|
if(!environment) {
|
||||||
|
return reject(new Error('Environment not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
environment.variables = variables;
|
||||||
|
|
||||||
|
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||||
|
|
||||||
|
collectionSchema
|
||||||
|
.validate(collectionToSave)
|
||||||
|
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||||
|
.then(() => dispatch(_saveEnvironment({variables, environmentUid, collectionUid})))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const removeLocalCollection = (collectionUid) => (dispatch, getState) => {
|
export const removeLocalCollection = (collectionUid) => (dispatch, getState) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -103,6 +103,18 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
saveEnvironment: (state, action) => {
|
||||||
|
const { variables, environmentUid, collectionUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
const environment = findEnvironmentInCollection(collection, environmentUid);
|
||||||
|
|
||||||
|
if(environment) {
|
||||||
|
environment.variables = variables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
newItem: (state, action) => {
|
newItem: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -727,6 +739,7 @@ export const {
|
|||||||
addEnvironment,
|
addEnvironment,
|
||||||
renameEnvironment,
|
renameEnvironment,
|
||||||
deleteEnvironment,
|
deleteEnvironment,
|
||||||
|
saveEnvironment,
|
||||||
newItem,
|
newItem,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
renameItem,
|
renameItem,
|
||||||
|
Loading…
Reference in New Issue
Block a user