feat: environment variables grid

This commit is contained in:
Anoop M D 2022-10-16 16:40:54 +05:30
parent 6a36313e0e
commit 2efc11ff6b
7 changed files with 278 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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