feat: connect environments to redux store

This commit is contained in:
Anoop M D 2022-10-16 05:46:49 +05:30
parent c6ac90a9f8
commit 7ca6270f2b
18 changed files with 307 additions and 115 deletions

View File

@ -1,25 +0,0 @@
import React, { useEffect, useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons";
import RenameEnvironment from "../../RenameEnvironment";
import DeleteEnvironment from "../../DeleteEnvironment";
// import StyledWrapper from "./StyledWrapper";
const EnvironmentDetails = ({selected}) => {
const [ openEditModal, setOpenEditModal] = useState(false);
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<div className="ml-10 flex-grow flex pt-4" style={{maxWidth: '700px'}}>
<span>{selected.name}</span>
<div className="flex gap-x-4 pl-4" >
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={selected} />}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={selected} />}
</div>
);
};
export default EnvironmentDetails;

View File

@ -1,43 +0,0 @@
import Modal from "components/Modal/index";
import React, { useState } from "react";
import CreateEnvironment from "./CreateEnvironment";
import Layout from "./Layout";
const EnvironmentSettings = ({onClose}) => {
const environments = [
{name: "My env", uid: 123},
{name: "hjdgfh dj", uid: 3876},
];
const [openCreateModal, setOpenCreateModal] = useState(false)
if(!environments.length) {
return (
<Modal
size="lg"
title="Environment"
confirmText={"Close"}
handleConfirm={onClose}
hideCancel={true}
>
<p>No environment found!</p>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={() => setOpenCreateModal(true)}>+ Create Environment</button>
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)}/>}
</Modal>
)
}
return (
<Modal
size="lg"
title="Environment"
confirmText={"Close"}
handleCancel={onClose}
hideFooter={true}
>
<Layout />
</Modal>
)
}
export default EnvironmentSettings;

View File

@ -1,10 +1,10 @@
import React, { useRef, forwardRef, useState } from 'react';
import Dropdown from 'components/Dropdown';
import { IconAdjustmentsHorizontal, IconCaretDown } from '@tabler/icons';
import EnvironmentSettings from "./EnvironmentSettings";
import EnvironmentSettings from "../EnvironmentSettings";
import StyledWrapper from './StyledWrapper';
const EnvironmentSelector = () => {
const EnvironmentSelector = ({collection}) => {
const dropdownTippyRef = useRef();
const [openSettingsModal, setOpenSettingsModal] = useState(false);
@ -46,7 +46,7 @@ const EnvironmentSelector = () => {
</div>
</Dropdown>
</div>
{openSettingsModal && <EnvironmentSettings onClose={() => setOpenSettingsModal(false)}/>}
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={() => setOpenSettingsModal(false)}/>}
</StyledWrapper>
);
};

View File

@ -1,12 +1,13 @@
import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index";
import Modal from "components/Modal/index";
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const CreateEnvironment = ({onClose}) => {
const CreateEnvironment = ({collection, onClose}) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
@ -17,12 +18,16 @@ const CreateEnvironment = ({onClose}) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
// dispatch(addWorkspace({name: values.name}));
// onClose();
dispatch(addEnvironment(values.name, collection.uid))
.then(() => {
toast.success("Environment created in collection");
onClose();
})
.catch(() => toast.error("An error occured while created the environment"));
}
});

View File

@ -1,15 +1,20 @@
import React from 'react';
import Portal from "components/Portal/index";
import toast from 'react-hot-toast';
import Modal from "components/Modal/index";
// import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { deleteEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const DeleteEnvironment = ({onClose, environment}) => {
const DeleteEnvironment = ({onClose, environment, collection}) => {
const dispatch = useDispatch();
const onConfirm = () =>{
// dispatch(deleteWorkspace({workspaceUid: workspace.uid}))
dispatch(deleteEnvironment(environment.uid, collection.uid))
.then(() => {
toast.success("Environment deleted successfully");
onClose();
})
.catch(() => toast.error("An error occured while deleting the environment"));
};
return (

View File

@ -0,0 +1,26 @@
import React, {useState } from "react";
import { IconEdit, IconTrash } from "@tabler/icons";
import RenameEnvironment from "../../RenameEnvironment";
import DeleteEnvironment from "../../DeleteEnvironment";
const EnvironmentDetails = ({environment, collection}) => {
const [ openEditModal, setOpenEditModal] = useState(false);
const [ openDeleteModal, setOpenDeleteModal] = useState(false);
return (
<div className="ml-6 flex-grow flex pt-6" style={{maxWidth: '700px'}}>
{openEditModal && <RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection}/>}
{openDeleteModal && <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection}/>}
<div className="flex flex-grow">
<div className="flex-grow font-medium">{environment.name}</div>
<div className="flex gap-x-4 pl-4 pr-6">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)}/>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)}/>
</div>
</div>
</div>
);
};
export default EnvironmentDetails;

View File

@ -1,12 +1,12 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
margin-left: -1rem;
margin-inline: -1rem;
margin-block: -1.5rem;
.environments-sidebar {
margin-bottom: 8px;
background-color: #ffffff;
min-height: 300px;
background-color: #eaeaea;
min-height: 400px;
}
.environment-item {
@ -15,28 +15,34 @@ const StyledWrapper = styled.div`
position: relative;
cursor: pointer;
padding: 8px 10px;
color: rgb(35, 35, 35);
border-bottom: 1px solid #eaecef;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: #f6f8fa;
background-color: #e4e4e4;
}
}
.active {
background-color: #e1e4e8;
background-color: #dcdcdc !important;
border-left: solid 2px var(--color-brand);
&:hover {
text-decoration: none;
background-color: #e1e4e8;
background-color: #dcdcdc !important;
}
}
.create-env {
.btn-create-environment {
padding: 8px 10px;
cursor: pointer;
border-bottom: none;
color: var(--color-text-link);
&:hover {
span {
text-decoration: underline;
}
}
}
`;

View File

@ -3,38 +3,43 @@ import EnvironmentDetails from "./EnvironmentDetails";
import CreateEnvironment from "../CreateEnvironment/index";
import StyledWrapper from "./StyledWrapper";
const environments = [
{name: "My env", uid: 123},
{name: "hjdgfh dj", uid: 3876},
{name: "hjdgfer dj", uid: 4678},
];
const Layout = () => {
const [selectedEnvironment, setSelectedEnvironment] = useState({});
const EnvironmentList = ({collection}) => {
const { environments } = collection;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
useEffect(() => {
setSelectedEnvironment(environments[0]);
}, []);
if(!selectedEnvironment) {
return null;
}
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)}/>}
<div className="flex">
<div style={{borderRight: "1px solid #ccc"}}>
<div>
<div className="environments-sidebar">
{environments && environments.length && environments.map((env) => (
<div className={selectedEnvironment.uid === env.uid ? "environment-item active": "environment-item"} onClick={() => setSelectedEnvironment(env)}>
<div
key={env.uid}
className={selectedEnvironment.uid === env.uid ? "environment-item active": "environment-item"}
onClick={() => setSelectedEnvironment(env)}
>
<span>{env.name}</span>
</div>
))}
<p className="create-env" onClick={() => setOpenCreateModal(true)}> + Create</p>
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
+ <span>Create</span>
</div>
</div>
<EnvironmentDetails selected={selectedEnvironment}/>
</div>
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)}/>}
<EnvironmentDetails environment={selectedEnvironment} collection={collection}/>
</div>
</StyledWrapper>
);
};
export default Layout;
export default EnvironmentList;

View File

@ -1,13 +1,14 @@
import React, { useEffect, useRef } from 'react';
import Portal from "components/Portal/index";
import Modal from "components/Modal/index";
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
// import { rename } from 'providers/ReduxStore/slices/environments';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
const RenameEnvironment = ({onClose, environment}) => {
// const dispatch = useDispatch();
const RenameEnvironment = ({onClose, environment, collection}) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
@ -17,12 +18,16 @@ const RenameEnvironment = ({onClose, environment}) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(30, 'must be 30 characters or less')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
// dispatch(rename({name: values.name, uid: environment.uid}));
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
.then(() => {
toast.success("Environment renamed successfully");
onClose();
})
.catch(() => toast.error("An error occured while renaming the environment"));
}
});

View File

@ -0,0 +1,13 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
button.btn-create-environment {
&:hover {
span {
text-decoration: underline;
}
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,50 @@
import Modal from "components/Modal/index";
import React, { useState } from "react";
import CreateEnvironment from "./CreateEnvironment";
import EnvironmentList from "./EnvironmentList";
import StyledWrapper from "./StyledWrapper";
const EnvironmentSettings = ({collection, onClose}) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false)
if(!environments || !environments.length) {
return (
<StyledWrapper>
<Modal
size="md"
title="Environments"
confirmText={"Close"}
handleConfirm={onClose}
handleCancel={onClose}
hideCancel={true}
>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)}/>}
<div className="text-center">
<p>No environments found!</p>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
+ <span>Create Environment</span>
</button>
</div>
</Modal>
</StyledWrapper>
)
}
return (
<Modal
size="lg"
title="Environments"
handleCancel={onClose}
hideFooter={true}
>
<EnvironmentList collection={collection}/>
</Modal>
)
}
export default EnvironmentSettings;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { IconFiles } from '@tabler/icons';
import EnvironmentSelector from 'components/EnvironmentSelector';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({collection}) => {
@ -12,7 +12,7 @@ const CollectionToolBar = ({collection}) => {
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<EnvironmentSelector />
<EnvironmentSelector collection={collection}/>
</div>
</div>
</StyledWrapper>

View File

@ -1,5 +1,5 @@
import path from 'path';
import axios from 'axios';
import filter from 'lodash/filter';
import each from 'lodash/each';
import trim from 'lodash/trim';
import toast from 'react-hot-toast';
@ -13,6 +13,7 @@ import {
transformRequestToSaveToFilesystem,
deleteItemInCollection,
findParentItemInCollection,
findEnvironmentInCollection,
isItemAFolder,
refreshUidsInItem
} from 'utils/collections';
@ -31,6 +32,9 @@ import {
cloneItem as _cloneItem,
deleteItem as _deleteItem,
saveRequest as _saveRequest,
addEnvironment as _addEnvironment,
renameEnvironment as _renameEnvironment,
deleteEnvironment as _deleteEnvironment,
createCollection as _createCollection,
renameCollection as _renameCollection,
deleteCollection as _deleteCollection,
@ -71,7 +75,8 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
const newCollection = {
uid: uuid(),
name: collectionName,
items: []
items: [],
environments: []
};
const requestItem = {
@ -606,6 +611,88 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
});
};
export const addEnvironment = (name, 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 environment = {
uid: uuid(),
name: name,
variables: []
};
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionToSave.environments = collectionToSave.environments || [];
collectionToSave.environments.push(environment);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_addEnvironment({environment, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const renameEnvironment = (newName, 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.name = newName;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_renameEnvironment({newName, environmentUid, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const deleteEnvironment = (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'));
}
collectionCopy.environments = filter(collectionCopy.environments, (e) => e.uid !== environmentUid);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_deleteEnvironment({environmentUid, collectionUid})))
.then(resolve)
.catch(reject);
});
};
export const removeLocalCollection = (collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();

View File

@ -11,6 +11,7 @@ import splitOnFirst from 'split-on-first';
import {
findCollectionByUid,
findItemInCollection,
findEnvironmentInCollection,
findItemInCollectionByPathname,
addDepth,
collapseCollection,
@ -69,6 +70,39 @@ export const collectionsSlice = createSlice({
deleteCollection: (state, action) => {
state.collections = filter(state.collections, c => c.uid !== action.payload.collectionUid);
},
addEnvironment: (state, action) => {
const { environment, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
collection.environments = collection.environments || [];
collection.environments.push(environment);
}
},
renameEnvironment: (state, action) => {
const { newName, environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if(environment) {
environment.name = newName;
}
}
},
deleteEnvironment: (state, action) => {
const { environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if(environment) {
collection.environments = filter(collection.environments, (e) => e.uid !== environmentUid);
}
}
},
newItem: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -690,6 +724,9 @@ export const {
renameCollection,
deleteCollection,
loadCollections,
addEnvironment,
renameEnvironment,
deleteEnvironment,
newItem,
deleteItem,
renameItem,

View File

@ -117,6 +117,10 @@ export const recursivelyGetAllItemUids = (items = []) => {
return map(flattenedItems, (i) => i.uid);
};
export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid);
}
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const copyHeaders = (headers) => {
return map(headers, (header) => {
@ -233,6 +237,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
collectionToSave.name = collection.name;
collectionToSave.uid = collection.uid;
collectionToSave.items = [];
collectionToSave.environments = collection.environments || [];
copyItems(collection.items, collectionToSave.items);

View File

@ -1,6 +1,21 @@
const Yup = require('yup');
const { uidSchema } = require("../common");
const environmentVariablesSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(),
value: Yup.string().nullable().max(2048, 'value must be 2048 characters or less').defined(),
type: Yup.string().oneOf(['text']).required('type is required'),
enabled: Yup.boolean().defined()
}).noUnknown(true).strict();
const environmentSchema = Yup.object({
uid: uidSchema,
name: Yup.string().min(1).max(50, 'name must be 50 characters or less').required('name is required'),
variables: Yup.array().of(environmentVariablesSchema).required('variables are required')
}).noUnknown(true).strict();
const keyValueSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less').defined(),
@ -55,6 +70,7 @@ const collectionSchema = Yup.object({
.max(50, 'name must be 100 characters or less')
.required('name is required'),
items: Yup.array().of(itemSchema),
environments: Yup.array().of(environmentSchema),
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
}).noUnknown(true).strict();