Config Manager related components placed in this repo

This commit is contained in:
2024-10-06 00:06:54 +05:30
parent d3cc948a57
commit d2ca38572b
37 changed files with 860 additions and 373 deletions

View File

@@ -0,0 +1,3 @@
.ar-ConfigRowItem {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigRowItem from "./ConfigRowItem"
describe("ConfigRowItem", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,97 @@
import { ChangeEvent, useState } from "react"
import { ArButtonVariants, ConfigRowItemProps } from "@armco/types"
import { Button, LoadableIcon, TextInput } from ".."
import "./ConfigRowItem.component.scss"
const ConfigRowItem = (props: ConfigRowItemProps): JSX.Element => {
const { config, disabled, isNew, onAdd, onUpdate, onDelete } = props
const [key, setKey] = useState<string>(config?.key || "")
const [value, setValue] = useState<string>(config?.value || "")
const [edited, setEdited] = useState<boolean>()
const configIsSubmittable = (isNew || edited) && key && value
return (
<div className="ar-ConfigRowItem row">
<div className="col-3">
<TextInput
value={key}
isDisabled={disabled && !edited}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setKey(e.target.value)
}
placeholder="Config key"
/>
</div>
<div className="col-4">
<TextInput
placeholder="Enter a value, text/json etc."
isDisabled={disabled && !edited}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
value={value}
/>
</div>
{isNew ? (
<div className="col-1 flex-v-center">
<Button
content="Add"
variant={ArButtonVariants.SUCCESS}
preIcon="io/IoMdAdd"
onClick={() => {
configIsSubmittable && onAdd && onAdd(key, value)
setKey("")
setValue("")
}}
/>
</div>
) : (
<div className="col-2 flex-v-center">
<span
className={`me-3${
configIsSubmittable ? " cursor-pointer" : " pe-none"
}`}
>
<LoadableIcon
icon="fa/FaCheck"
color={configIsSubmittable ? "green" : "rgba(0, 128, 0, 0.3)"}
onClick={() => {
config?._id &&
configIsSubmittable &&
onUpdate &&
onUpdate(config?._id, key, value)
setKey("")
setValue("")
}}
hoverShadow
/>
</span>
<span className={`me-3${edited ? " pe-none" : " cursor-pointer"}`}>
<LoadableIcon
icon={edited ? "rx/RxCross2" : "md/MdModeEditOutline"}
color={isNew || edited ? "rgba(165, 42, 42, 0.3)" : "brown"}
onClick={() => (edited ? setEdited(false) : setEdited(true))}
/>
</span>
<span className={`me-3 ${edited ? "pe-none" : "cursor-pointer"}`}>
<LoadableIcon
icon="md/MdAdd"
width="1.3rem"
color={!configIsSubmittable ? "green" : "rgba(0, 128, 0, 0.3)"}
onClick={() => onAdd && onAdd(key, value)}
/>
</span>
<span className={edited ? "pe-none" : "cursor-pointer"}>
<LoadableIcon
icon="ri/RiDeleteBin6Line"
color={isNew || edited ? "rgba(128, 0, 0, 0.3)" : "red"}
onClick={() => onDelete && onDelete(config?._id)}
/>
</span>
</div>
)}
</div>
)
}
export default ConfigRowItem

View File

@@ -0,0 +1,3 @@
import ConfigRowItem from "./ConfigRowItem"
export default ConfigRowItem

View File

@@ -0,0 +1,3 @@
.ar-ConfigurationList {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationList from "./ConfigurationList"
describe("ConfigurationList", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,28 @@
import { useNavigate } from "react-router-dom"
import { ConfigurationListProps, TreeListData } from "@armco/types"
import { Network, TreeList } from ".."
import "./ConfigurationList.component.scss"
const ConfigurationList = (props: ConfigurationListProps): JSX.Element => {
const { list } = props
const navigate = useNavigate()
const handleComponentSelect = (treeNode: TreeListData) => {
const params = treeNode.data?.props
treeNode.data?.component &&
navigate(
Network.stringifyUrl(`/components/${treeNode.data?.component}`, params),
)
}
return (
<div className="ar-ConfigurationList w-100 p-2">
<TreeList
onItemSelect={handleComponentSelect}
data={list || []}
title="Configurations"
firstExpanded={true}
/>
</div>
)
}
export default ConfigurationList

View File

@@ -0,0 +1,3 @@
import ConfigurationList from "./ConfigurationList"
export default ConfigurationList

View File

@@ -0,0 +1,10 @@
.ar-ConfigurationLoginPrompt {
width: 50vw;
background-color: var(--ar-bg);
border-radius: 0.25rem;
small {
color: var(--ar-color-secondary);
line-height: 1rem;
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
describe("ConfigurationLoginPrompt", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,48 @@
import { ArButtonVariants, ConfigurationLoginPromptProps } from "@armco/types"
import { Button, LoadableIcon, setRightPanelContent, useAppDispatch } from ".."
import "./ConfigurationLoginPrompt.component.scss"
const ConfigurationLoginPrompt = (
props: ConfigurationLoginPromptProps,
): JSX.Element => {
const dispatch = useAppDispatch()
return (
<div className="ar-ConfigurationLoginPrompt p-3 border">
<div className="flex-h-center flex-column">
<h6 className="flex-v-center" style={{ color: "#ffbf00" }}>
<LoadableIcon
classes="me-2"
icon="fa/FaExclamationTriangle"
color="#ffbf00"
/>
Please login to continue
</h6>
<small>
This is the configurations feature where you can create and save
configurations as key-value pairs.
</small>
<small>
These key-value pairs can then be accessed using an endpoint inside
your application.
</small>
<small className="mb-3">
<strong>
In order to be able to save and secure these configurations, you
need to create an account, if one doesn't exist already and login
using the same. Please use the button below to continue.
</strong>
</small>
</div>
<Button
content="Login"
variant={ArButtonVariants.SUCCESS}
postIcon="io5/IoArrowForwardCircle"
onClick={() => {
dispatch(setRightPanelContent({ name: "LoginProvider" }))
}}
/>
</div>
)
}
export default ConfigurationLoginPrompt

View File

@@ -0,0 +1,3 @@
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
export default ConfigurationLoginPrompt

View File

@@ -0,0 +1,3 @@
.ar-ConfigurationNoConfigPrompt {
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
describe("ConfigurationNoConfigPrompt", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,102 @@
import { ReactNode } from "react"
import {
ArButtonVariants,
ArPopoverSlots,
ArPopoverTriggers,
ConfigurationNoConfigPromptProps,
} from "@armco/types"
import {
Button,
LoadableIcon,
NamespaceOrgForm,
Popover,
setModalState,
useAppDispatch,
} from ".."
import "./ConfigurationNoConfigPrompt.component.scss"
const ConfigurationNoConfigPrompt = (
props: ConfigurationNoConfigPromptProps,
): ReactNode => {
const dispatch = useAppDispatch()
return (
<div className="ar-ConfigurationNoConfigPrompt flex-center flex-column border px-3 py-5 w-50">
<LoadableIcon
classes="mb-4"
icon="gr/GrConfigure"
size="7rem"
color="lightgrey"
/>
<p className="ar-ConfigurationNoConfigPrompt__message mx-5">
You don't have any configurations created yet, you can start by
selecting one of the options below
</p>
<div className="ar-ConfigurationNoConfigPrompt__buttons d-flex">
<Popover trigger={ArPopoverTriggers.HOVER}>
<Button
classes="me-3"
content="+ Space"
variant={ArButtonVariants.SUCCESS}
slot={ArPopoverSlots.ANCHOR}
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="namespace" />,
closeHandler: () => dispatch(setModalState({ show: false })),
}),
)
}
/>
<div
slot={ArPopoverSlots.POPOVER}
className="text-wrap"
style={{ maxWidth: "10rem" }}
>
<p>This is where you keep your configurations.</p>
<p className="mb-3">
Use namespaces in your project/s to avoid fetching all the
configurations for multiple projects.
</p>
<b>Namespace is required to create a configuration</b>
</div>
</Popover>
<Popover trigger={ArPopoverTriggers.HOVER}>
<Button
variant={ArButtonVariants.SUCCESS}
content="+ Organization"
slot={ArPopoverSlots.ANCHOR}
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="org" />,
closeHandler: () => dispatch(setModalState({ show: false })),
}),
)
}
/>
<div
slot={ArPopoverSlots.POPOVER}
className="text-wrap"
style={{ maxWidth: "10rem" }}
>
<p>
You may skip creating an organization as one will be created for
you if you don't.
</p>
<p>
Or you may choose to create one. This will allow you to control
severl aspects of an organization at creation time itself
</p>
</div>
</Popover>
</div>
</div>
)
}
export default ConfigurationNoConfigPrompt

View File

@@ -0,0 +1,3 @@
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
export default ConfigurationNoConfigPrompt

View File

@@ -0,0 +1,13 @@
.ar-ConfigurationViewer {
background-color: var(--ar-bg);
.ar-ConfigurationViewer__header {
background-color: var(--ar-bg-base);
border-bottom: 1px solid var(--ar-color-layout-border);
}
input:disabled {
border: none;
background: transparent;
font-weight: bold;
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import ConfigurationViewer from "./ConfigurationViewer"
describe("ConfigurationViewer", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,210 @@
import { v4 as uuid } from "uuid"
import {
ArAlertType,
ArButtonVariants,
ArPopoverPositions,
ArPopoverSlots,
ArSizes,
ConfigurationViewerProps,
ObjectType,
} from "@armco/types"
import {
API_CONFIG,
Breadcrumb,
Button,
ConfigRowItem,
ENDPOINTS,
InlineMenu,
LoadableIcon,
NamespaceInfoBox,
NamespaceOrgForm,
Network,
notify,
Popover,
setModalState,
useAppDispatch,
} from ".."
import "./ConfigurationViewer.component.scss"
const ConfigurationViewer = (props: ConfigurationViewerProps): JSX.Element => {
const { namespace, fetchNamespaces } = props
const configurations = namespace?.configs
const dispatch = useAppDispatch()
const addConfig = (key: string, value: string, _id?: string) => {
const payload: ObjectType = {
key,
value,
namespace: namespace._id,
version: "v1",
}
_id && (payload._id = _id)
Network.post(
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.CONFIG.ROOT +
ENDPOINTS.CONFIG.SAVE +
namespace._id,
payload,
)
.then((res) => {
if (res.status === 200) {
fetchNamespaces()
dispatch(
notify({
show: true,
message: "Added a new config successfully",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
}
})
.catch((e) => {
dispatch(
notify({
show: true,
message: "Failed to add new config",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}
const deleteConfig = (_id?: string) => {
Network.get(
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.CONFIG.ROOT +
ENDPOINTS.CONFIG.DELETE +
"/" +
namespace._id +
"/" +
_id,
)
.then((res) => {
if (res.status === 200) {
fetchNamespaces()
dispatch(
notify({
show: true,
message: "Removed config successfully",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
}
})
.catch((e) => {
dispatch(
notify({
show: true,
message: "Failed to delete config",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}
return (
<div className="ar-ConfigurationViewer d-flex flex-column w-100 h-100">
<div className="ar-ConfigurationViewer__header px-3 py-2 w-100">
<Breadcrumb
classes="d-inline-block"
data={[{ label: namespace.name, route: `/config/${namespace.name}` }]}
/>
<Button
classes="float-end"
content="Edit"
color="brown"
preIcon="fa/FaRegEdit"
variant={ArButtonVariants.LINK}
size={ArSizes.SMALL}
/>
<Popover
classes="float-end"
// trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.BOTTOMLEFT}
version="v1"
>
<Button
content="Add"
color="green"
preIcon="md/MdAddCircleOutline"
variant={ArButtonVariants.LINK}
size={ArSizes.SMALL}
slot={ArPopoverSlots.ANCHOR}
/>
<InlineMenu
slot={ArPopoverSlots.POPOVER}
data={{
Org: {},
Namespace: {
onClick: () =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceOrgForm context="namespace" />,
closeHandler: () =>
dispatch(setModalState({ show: false })),
}),
),
},
}}
/>
</Popover>
<LoadableIcon
classes="cursor-pointer"
icon="io/IoMdInformationCircle"
color="#0d6efd"
onClick={() =>
dispatch(
setModalState({
show: true,
isSticky: true,
content: <NamespaceInfoBox namespace={namespace} />,
closeHandler: () => dispatch(setModalState({ show: false })),
}),
)
}
/>
</div>
<div className="ar-ConfigurationViewer__content flex-1 d-flex px-3 py-2 w-100">
<div className="d-flex flex-1 flex-column">
<ConfigRowItem onAdd={addConfig} isNew />
<div className="my-3 border" />
{configurations && configurations.length > 0 ? (
configurations.map((configuration) => {
return (
<ConfigRowItem
onUpdate={(_id: string, key: string, value: string) =>
addConfig(key, value, _id)
}
onDelete={deleteConfig}
config={configuration}
disabled
/>
)
})
) : (
<div className="row flex-1">
<div className="col flex-center fw-bold">
Nothing here, start by adding configurations above
</div>
</div>
)}
</div>
</div>
<div className="ar-ConfigurationViewer__footer px-3 py-2 w-100">
<Button
classes="float-end"
content="Submit"
variant={ArButtonVariants.SUCCESS}
size={ArSizes.SMALL}
/>
</div>
</div>
)
}
export default ConfigurationViewer

View File

@@ -0,0 +1,3 @@
import ConfigurationViewer from "./ConfigurationViewer"
export default ConfigurationViewer

View File

@@ -0,0 +1,9 @@
.ar-NamespaceInfoBox {
.ar-NamespaceInfoBox__namespace-link {
border: var(--ar-border);
background-color: var(--ar-bg-hover);
border-radius: 3px;
font-size: 0.75rem;
cursor: pointer;
}
}

View File

@@ -0,0 +1,8 @@
import React from "react"
import NamespaceInfoBox from "./NamespaceInfoBox"
describe("NamespaceInfoBox", () => {
it("renders without error", () => {
})
})

View File

@@ -0,0 +1,128 @@
import { useRef, useState } from "react"
import { v4 as uuid } from "uuid"
import hljs from "highlight.js/lib/core"
import javascript from "highlight.js/lib/languages/javascript"
import { NamespaceInfoBoxProps } from "@armco/types"
import {
Alert,
API_CONFIG,
ENDPOINTS,
Helper,
Link,
LoadableIcon,
Text,
} from ".."
import "highlight.js/styles/github.css"
import "./NamespaceInfoBox.component.scss"
hljs.registerLanguage("javascript", javascript)
const NamespaceInfoBox = (props: NamespaceInfoBoxProps): JSX.Element => {
const { namespace } = props
const codeRef = useRef<HTMLPreElement>(null)
const [displayed, show] = useState<{ displayed: boolean }>()
const [message, setMessage] = useState<string>()
const helpText =
"Now that your namespace has been created you can integrate it with your project using the below endpoint"
const nsLink =
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.NAMESPACE.ROOT +
ENDPOINTS.NAMESPACE.FETCH +
namespace._id
const nsAllLink =
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.NAMESPACE.ROOT +
ENDPOINTS.NAMESPACE.FETCHALL
const codeStr = `
// API to fetch configurations for the namespace ${namespace.name}
fetch("${nsLink}")
.then((res) => {
if (res.status === 200) {
console.log(res.body.namespace);
}
})
`
const code = hljs.highlight("javascript", codeStr).value
return (
<div className="ar-NamespaceInfoBox w-100">
<div className="ar-NamespaceInfoBox__content d-flex flex-1 flex-column h-100">
<header className="ar-NamespaceInfoBox__header w-100 border-bottom px-3 py-2 fw-bold f4 flex-v-center">
{namespace.name}
</header>
<main className="ar-NamespaceInfoBox__body flex-1 overflow-auto">
<div className="ar-NameSpaceModal__body p-3 h-100 flex-center flex-column">
<Text
classes="ar-help-text mb-3 w-100 text-wrap"
descriptor={{
id: "temp-text-id",
order: 0,
name: "Text",
text: helpText,
chunks: {},
}}
/>
<Link
to={nsLink}
label={nsLink}
classes="ar-NamespaceInfoBox__namespace-link px-3 py-2 mb-3"
onClick={() => {
Helper.copyOrPrompt(nsLink, () => {
show({ displayed: true })
setMessage("Link Copied!")
})
}}
preemptNavigation
/>
<small className="ar-help-text mb-3">
Or to fetch all of your namespaces
</small>
<Link
to={nsAllLink}
label={nsAllLink}
classes="ar-NamespaceInfoBox__namespace-link px-3 py-2 mb-3"
onClick={() => {
Helper.copyOrPrompt(nsAllLink, () => {
show({ displayed: true })
setMessage("Link Copied!")
})
}}
preemptNavigation
/>
<div className="ar-NamespaceInfoBox__sample-code border p-2 w-100 overflow-auto position-relative">
<LoadableIcon
classes="ar-NamespaceInfoBox__copy-button position-absolute"
icon="md/MdContentCopy"
color="grey"
hoverColor="black"
onClick={() => {
const code = codeRef.current?.innerText
Helper.copyOrPrompt(code, () => {
show({ displayed: true })
setMessage(
"Copied code for integrating your configurations",
)
})
}}
/>
<pre>
<code
dangerouslySetInnerHTML={{ __html: code }}
ref={codeRef}
/>
</pre>
</div>
</div>
</main>
<Alert
show={displayed?.displayed}
message={message || "Link Copied!"}
uid={uuid()}
closeable
/>
</div>
</div>
)
}
export default NamespaceInfoBox

View File

@@ -0,0 +1,3 @@
import NamespaceInfoBox from "./NamespaceInfoBox"
export default NamespaceInfoBox

View File

@@ -0,0 +1,13 @@
.ar-NamespaceOrgForm {
header {
min-height: var(--ar-height-header);
height: unset;
background-color: var(--ar-bg-base);
}
footer {
min-height: var(--ar-height-footer);
height: unset;
background-color: var(--ar-bg-base);
}
}

View File

@@ -0,0 +1,5 @@
import NamespaceOrgForm from "./NamespaceOrgForm"
describe("NamespaceOrgForm", () => {
it("renders without error", () => {})
})

View File

@@ -0,0 +1,119 @@
import { ChangeEvent, useState } from "react"
import { v4 as uuid } from "uuid"
import {
ArAlertType,
ArButtonVariants,
ArSizes,
FunctionType,
NamespaceOrgFormProps,
} from "@armco/types"
import {
API_CONFIG,
Button,
ENDPOINTS,
Loader,
NamespaceInfoBox,
Network,
notify,
setModalState,
TextInput,
useAppDispatch,
} from ".."
import "./NamespaceOrgForm.component.scss"
const NamespaceOrgForm = (props: NamespaceOrgFormProps): JSX.Element => {
const { context } = props
const [namespace, setNamespace] = useState<string>()
const [org, setOrg] = useState<string>()
const [loading, setLoading] = useState<boolean>()
const dispatch = useAppDispatch()
const nsURL =
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.NAMESPACE.ROOT +
ENDPOINTS.NAMESPACE.CREATE
const helpText =
context === "org"
? "You may think of an organization as a way to organize your configuration spaces. A typical use case could be managing configurations for multiple projects, and maintaining configuration for each project in it's own organization. An organization may contain one or more organizations and/or namespaces"
: "You may enter a name that indicates the purpose of the configuration held under this space, for eg. an environment name - test, pre-prod, prod; or a space for form field configurations, eg. field_definitions, etc."
return (
<div className="ar-NamespaceOrgForm">
{loading && <Loader />}
<div className="ar-Modal__content d-flex flex-1 flex-column">
<header className="ar-Modal__header w-100 border-bottom px-3 py-2 fw-bold f4 flex-v-center">
{context === "org" ? "Create Organization" : "Create a Space"}
</header>
<main className="ar-Modal__body flex-1">
<div className="ar-NameSpaceModal__body p-3 h-100 flex-center flex-column">
<TextInput
containerClasses="mb-3"
classes="w-100"
label={context === "org" ? "Organization Name" : "Namespace"}
onChange={
((event: ChangeEvent<HTMLInputElement>) =>
(context === "org" ? setOrg : setNamespace)(
event.target.value,
)) as FunctionType
}
/>
<small className="ar-help-text text-wrap">{helpText}</small>
</div>
</main>
<footer className="ar-Modal__footer w-100 border-top px-3 py-2 d-flex justify-content-end">
<Button
classes="me-3"
variant={ArButtonVariants.PRIMARY}
size={ArSizes.SMALL}
content="Submit"
onClick={() => {
setLoading(true)
Network.post(nsURL, { org, namespace })
.then((res) => {
console.log(res)
setLoading(false)
if (res.status === 200) {
dispatch(
notify({
show: true,
message: "Namespace created successfully!",
type: ArAlertType.SUCCESS,
uid: uuid(),
}),
)
dispatch(
setModalState({
content: namespace ? (
<NamespaceInfoBox namespace={res.body.namespace} />
) : null,
}),
)
}
})
.catch((err) => {
setLoading(false)
dispatch(
notify({
show: true,
message: "Failed to create namespace/org",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}}
/>
<Button
variant={ArButtonVariants.SECONDARY}
size={ArSizes.SMALL}
content="Cancel"
onClick={() => dispatch(setModalState({ show: false }))}
/>
</footer>
</div>
</div>
)
}
export default NamespaceOrgForm

View File

@@ -0,0 +1,3 @@
import NamespaceOrgForm from "./NamespaceOrgForm"
export default NamespaceOrgForm

View File

@@ -1,14 +0,0 @@
.ar-ConfigurationsPage {
.ar-ConfigurationNoConfigPrompt {
background-color: var(--ar-bg-base);
.ar-ConfigurationNoConfigPrompt__message {
line-height: 1.5rem;
text-align: center;
}
}
.ar-ConfigurationPage__tree-badge {
top: -10px;
}
}

View File

@@ -1,18 +0,0 @@
import { createSlice } from "@reduxjs/toolkit"
export interface ConfigurationsPageState {}
const initialState: ConfigurationsPageState = {}
export const configurationsPageSlice = createSlice({
name: "configurationsPage",
initialState,
reducers: {
increment: (state) => {},
},
extraReducers: (builder) => {},
})
export const { increment } = configurationsPageSlice.actions
export default configurationsPageSlice.reducer

View File

@@ -1,3 +0,0 @@
describe("ConfigurationsPage", () => {
it("renders without error", () => {})
})

View File

@@ -1,144 +0,0 @@
import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { v4 as uuid } from "uuid"
import {
ArAlertType,
ArBadgeType,
ModalProps,
Namespace,
ObjectType,
TreeListData,
} from "@armco/types"
import Main from "../../components/Main"
import Footer from "../../components/molecules/Footer"
import {
API_CONFIG,
ConfigurationList,
ConfigurationLoginPrompt,
ConfigurationNoConfigPrompt,
ConfigurationViewer,
ENDPOINTS,
ErrorBoundary,
getLoggedIn,
getModalState,
getRightPanelContent,
Modal,
Network,
notify,
useAppDispatch,
useAppSelector,
} from "../../components"
import "./ConfigurationsPage.page.scss"
const ConfigurationsPage = (): JSX.Element => {
const navigate = useNavigate()
const location = useLocation()
const [namespaces, setNamespaces] = useState<Array<Namespace>>()
const [sidebarJuice, setSidebarJuice] = useState<Array<TreeListData>>()
const [namespace, setSelectedNamespace] = useState<Namespace>()
const isLoggedIn = useAppSelector<boolean | undefined>(getLoggedIn)
const rightPanelContent = useAppSelector<
| { name?: string; props?: ObjectType; component?: JSX.Element | null }
| undefined
>(getRightPanelContent)
const modalState = useAppSelector<ModalProps | undefined>(getModalState)
const dispatch = useAppDispatch()
useEffect(() => {
const orgTree: Array<TreeListData> = []
const onItemSelect = (tld: TreeListData) => {
const ns = tld.data
// setSelectedNamespace(orgTree[0].children[0].data)
navigate("/config/" + ns._id, { replace: true, state: ns })
}
namespaces &&
namespaces.forEach((namespace) => {
const org = namespace.org
if (org._id) {
let orgTreeItem = orgTree.find((item) => item.label === org.name)
if (!orgTreeItem) {
orgTreeItem = { label: org.name, data: org, children: [] }
orgTree.push(orgTreeItem)
}
orgTreeItem.children?.push({
label: namespace.name,
data: namespace,
badges: [
{
text: "NS",
type: ArBadgeType.COMPLETE,
},
],
onItemSelect,
})
}
})
setSidebarJuice(orgTree)
const ns = orgTree[0] && orgTree[0].children && orgTree[0].children[0].data
ns && navigate("/config/" + ns._id, { replace: true, state: ns })
}, [namespaces])
useEffect(() => {
const urlParts = location.pathname.split("/").filter((part) => !!part)
if (urlParts.length > 1 && !!location.state) {
setSelectedNamespace(location.state)
}
}, [location])
useEffect(() => {
isLoggedIn && fetchNamespaces()
}, [isLoggedIn])
const fetchNamespaces = () => {
Network.get(
API_CONFIG.CONFIG[process.env.NODE_ENV] +
ENDPOINTS.NAMESPACE.ROOT +
ENDPOINTS.NAMESPACE.FETCHALL,
)
.then((res) => {
if (res.status === 200) {
setNamespaces(res.body)
}
})
.catch((e) => {
dispatch(
notify({
show: true,
message: "Failed to fetch your spaces",
type: ArAlertType.ERROR,
uid: uuid(),
}),
)
})
}
return (
<div className="ar-ConfigurationsPage">
<Main
contentClasses="p-2"
drawerContent={isLoggedIn && <ConfigurationList list={sidebarJuice} />}
mainContent={
<ErrorBoundary>
{isLoggedIn ? (
namespace ? (
<ConfigurationViewer
namespace={namespace}
fetchNamespaces={fetchNamespaces}
/>
) : (
<ConfigurationNoConfigPrompt />
)
) : (
<ConfigurationLoginPrompt />
)}
</ErrorBoundary>
}
rightPanelContent={rightPanelContent}
/>
<Modal {...modalState} />
<Footer />
</div>
)
}
export default ConfigurationsPage

View File

@@ -1,3 +0,0 @@
import ConfigurationsPage from "./ConfigurationsPage"
export default ConfigurationsPage

View File

@@ -1,67 +0,0 @@
import { useState } from "react"
import { useAppSelector, useAppDispatch } from "../../app/hooks"
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from "./counterSlice"
export function Counter() {
const count = useAppSelector(selectCount)
const dispatch = useAppDispatch()
const [incrementAmount, setIncrementAmount] = useState("2")
const incrementValue = Number(incrementAmount) || 0
return (
<div>
<div className="row">
<button
className="button"
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className="value">{count}</span>
<button
className="button"
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className="row">
<input
className="textbox"
aria-label="Set increment amount"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
className="button"
onClick={() => dispatch(incrementByAmount(incrementValue))}
>
Add Amount
</button>
<button
className="asyncButton"
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
<button
className="button"
onClick={() => dispatch(incrementIfOdd(incrementValue))}
>
Add If Odd
</button>
</div>
</div>
)
}

View File

@@ -1,6 +0,0 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500),
)
}

View File

@@ -1,34 +0,0 @@
import counterReducer, {
CounterState,
increment,
decrement,
incrementByAmount,
} from "./counterSlice"
describe("counter reducer", () => {
const initialState: CounterState = {
value: 3,
status: "idle",
}
it("should handle initial state", () => {
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
value: 0,
status: "idle",
})
})
it("should handle increment", () => {
const actual = counterReducer(initialState, increment())
expect(actual.value).toEqual(4)
})
it("should handle decrement", () => {
const actual = counterReducer(initialState, decrement())
expect(actual.value).toEqual(2)
})
it("should handle incrementByAmount", () => {
const actual = counterReducer(initialState, incrementByAmount(2))
expect(actual.value).toEqual(5)
})
})

View File

@@ -1,84 +0,0 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
import { RootState, AppThunk } from "../../app/store"
import { fetchCount } from "./counterAPI"
export interface CounterState {
value: number
status: "idle" | "loading" | "failed"
}
const initialState: CounterState = {
value: 0,
status: "idle",
}
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
"counter/fetchCount",
async (amount: number) => {
const response = await fetchCount(amount)
// The value we return becomes the `fulfilled` action payload
return response.data
},
)
export const counterSlice = createSlice({
name: "counter",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn"t actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = "loading"
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = "idle"
state.value += action.payload
})
.addCase(incrementAsync.rejected, (state) => {
state.status = "failed"
})
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they"re used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value
// We can also write thunks by hand, which may contain both sync and async logic.
// Here"s an example of conditionally dispatching actions based on current state.
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState())
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount))
}
}
export default counterSlice.reducer