Config Manager related components placed in this repo
This commit is contained in:
3
src/app/components/ConfigRowItem/ConfigRowItem.component.scss
Executable file
3
src/app/components/ConfigRowItem/ConfigRowItem.component.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.ar-ConfigRowItem {
|
||||
|
||||
}
|
||||
8
src/app/components/ConfigRowItem/ConfigRowItem.test.ts
Executable file
8
src/app/components/ConfigRowItem/ConfigRowItem.test.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import ConfigRowItem from "./ConfigRowItem"
|
||||
|
||||
describe("ConfigRowItem", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
97
src/app/components/ConfigRowItem/ConfigRowItem.tsx
Executable file
97
src/app/components/ConfigRowItem/ConfigRowItem.tsx
Executable 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
|
||||
3
src/app/components/ConfigRowItem/index.ts
Executable file
3
src/app/components/ConfigRowItem/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import ConfigRowItem from "./ConfigRowItem"
|
||||
|
||||
export default ConfigRowItem
|
||||
3
src/app/components/ConfigurationList/ConfigurationList.component.scss
Executable file
3
src/app/components/ConfigurationList/ConfigurationList.component.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.ar-ConfigurationList {
|
||||
|
||||
}
|
||||
8
src/app/components/ConfigurationList/ConfigurationList.test.ts
Executable file
8
src/app/components/ConfigurationList/ConfigurationList.test.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import ConfigurationList from "./ConfigurationList"
|
||||
|
||||
describe("ConfigurationList", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
28
src/app/components/ConfigurationList/ConfigurationList.tsx
Executable file
28
src/app/components/ConfigurationList/ConfigurationList.tsx
Executable 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
|
||||
3
src/app/components/ConfigurationList/index.ts
Executable file
3
src/app/components/ConfigurationList/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import ConfigurationList from "./ConfigurationList"
|
||||
|
||||
export default ConfigurationList
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
|
||||
|
||||
describe("ConfigurationLoginPrompt", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
48
src/app/components/ConfigurationLoginPrompt/ConfigurationLoginPrompt.tsx
Executable file
48
src/app/components/ConfigurationLoginPrompt/ConfigurationLoginPrompt.tsx
Executable 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
|
||||
3
src/app/components/ConfigurationLoginPrompt/index.ts
Executable file
3
src/app/components/ConfigurationLoginPrompt/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import ConfigurationLoginPrompt from "./ConfigurationLoginPrompt"
|
||||
|
||||
export default ConfigurationLoginPrompt
|
||||
@@ -0,0 +1,3 @@
|
||||
.ar-ConfigurationNoConfigPrompt {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
|
||||
|
||||
describe("ConfigurationNoConfigPrompt", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
102
src/app/components/ConfigurationNoConfigPrompt/ConfigurationNoConfigPrompt.tsx
Executable file
102
src/app/components/ConfigurationNoConfigPrompt/ConfigurationNoConfigPrompt.tsx
Executable 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
|
||||
3
src/app/components/ConfigurationNoConfigPrompt/index.ts
Executable file
3
src/app/components/ConfigurationNoConfigPrompt/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import ConfigurationNoConfigPrompt from "./ConfigurationNoConfigPrompt"
|
||||
|
||||
export default ConfigurationNoConfigPrompt
|
||||
13
src/app/components/ConfigurationViewer/ConfigurationViewer.component.scss
Executable file
13
src/app/components/ConfigurationViewer/ConfigurationViewer.component.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
8
src/app/components/ConfigurationViewer/ConfigurationViewer.test.ts
Executable file
8
src/app/components/ConfigurationViewer/ConfigurationViewer.test.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import ConfigurationViewer from "./ConfigurationViewer"
|
||||
|
||||
describe("ConfigurationViewer", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
210
src/app/components/ConfigurationViewer/ConfigurationViewer.tsx
Executable file
210
src/app/components/ConfigurationViewer/ConfigurationViewer.tsx
Executable 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
|
||||
3
src/app/components/ConfigurationViewer/index.ts
Executable file
3
src/app/components/ConfigurationViewer/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import ConfigurationViewer from "./ConfigurationViewer"
|
||||
|
||||
export default ConfigurationViewer
|
||||
9
src/app/components/NamespaceInfoBox/NamespaceInfoBox.component.scss
Executable file
9
src/app/components/NamespaceInfoBox/NamespaceInfoBox.component.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
8
src/app/components/NamespaceInfoBox/NamespaceInfoBox.test.ts
Executable file
8
src/app/components/NamespaceInfoBox/NamespaceInfoBox.test.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import NamespaceInfoBox from "./NamespaceInfoBox"
|
||||
|
||||
describe("NamespaceInfoBox", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
128
src/app/components/NamespaceInfoBox/NamespaceInfoBox.tsx
Executable file
128
src/app/components/NamespaceInfoBox/NamespaceInfoBox.tsx
Executable 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
|
||||
3
src/app/components/NamespaceInfoBox/index.ts
Executable file
3
src/app/components/NamespaceInfoBox/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import NamespaceInfoBox from "./NamespaceInfoBox"
|
||||
|
||||
export default NamespaceInfoBox
|
||||
13
src/app/components/NamespaceOrgForm/NamespaceOrgForm.component.scss
Executable file
13
src/app/components/NamespaceOrgForm/NamespaceOrgForm.component.scss
Executable 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);
|
||||
}
|
||||
}
|
||||
5
src/app/components/NamespaceOrgForm/NamespaceOrgForm.test.ts
Executable file
5
src/app/components/NamespaceOrgForm/NamespaceOrgForm.test.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import NamespaceOrgForm from "./NamespaceOrgForm"
|
||||
|
||||
describe("NamespaceOrgForm", () => {
|
||||
it("renders without error", () => {})
|
||||
})
|
||||
119
src/app/components/NamespaceOrgForm/NamespaceOrgForm.tsx
Executable file
119
src/app/components/NamespaceOrgForm/NamespaceOrgForm.tsx
Executable 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
|
||||
3
src/app/components/NamespaceOrgForm/index.ts
Executable file
3
src/app/components/NamespaceOrgForm/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import NamespaceOrgForm from "./NamespaceOrgForm"
|
||||
|
||||
export default NamespaceOrgForm
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
describe("ConfigurationsPage", () => {
|
||||
it("renders without error", () => {})
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
import ConfigurationsPage from "./ConfigurationsPage"
|
||||
|
||||
export default ConfigurationsPage
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user