diff --git a/public/locales/en.js b/public/locales/en.js
index 767a9293..98f4d3cd 100644
--- a/public/locales/en.js
+++ b/public/locales/en.js
@@ -1,6 +1,9 @@
export default {
en: {
translation: {
+ aws: {
+ ssmParameter: 'SSM Parameter'
+ },
common: {
all: 'All',
cancel: 'Cancel',
@@ -44,6 +47,7 @@ export default {
textClass: 'Text Class',
turnOff: 'Turn Off',
turnOn: 'Turn On',
+ type: 'Type',
updatedBy: 'Updated By',
value: 'Value',
welcome: 'Welcome'
diff --git a/src/js/views/Project/Configuration.jsx b/src/js/views/Project/Configuration.jsx
deleted file mode 100644
index 51e4ae0b..00000000
--- a/src/js/views/Project/Configuration.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useContext, useEffect } from 'react'
-
-import { Context } from '../../state'
-import { WishedFutureState } from '../../components'
-
-function Configuration({ urlPath }) {
- const [state, dispatch] = useContext(Context)
- useEffect(() => {
- dispatch({
- type: 'SET_CURRENT_PAGE',
- payload: {
- title: 'common.configuration',
- url: new URL(`${urlPath}/configuration`, state.baseURL)
- }
- })
- }, [])
- return (
-
-
- This tab will provide an abstracted interface for editing the
- configuration in Consul, Vault, AWS SSM Parameter Store, and K8s
- Configuration Maps.
-
-
- )
-}
-Configuration.propTypes = {
- urlPath: PropTypes.string.isRequired
-}
-export { Configuration }
diff --git a/src/js/views/Project/Configuration/Configuration.jsx b/src/js/views/Project/Configuration/Configuration.jsx
new file mode 100644
index 00000000..d05e8e62
--- /dev/null
+++ b/src/js/views/Project/Configuration/Configuration.jsx
@@ -0,0 +1,40 @@
+import React, { useContext, useEffect } from 'react'
+import { Context } from '../../../state'
+import PropTypes from 'prop-types'
+import { SSMConfiguration } from './SSMConfiguration'
+import { WishedFutureState } from '../../../components'
+
+function Configuration({ urlPath, project }) {
+ const [globalState, dispatch] = useContext(Context)
+
+ useEffect(() => {
+ dispatch({
+ type: 'SET_CURRENT_PAGE',
+ payload: {
+ title: 'common.configuration',
+ url: new URL(`${urlPath}/configuration`, globalState.baseURL)
+ }
+ })
+ }, [])
+
+ switch (project.configuration_type) {
+ case 'ssm':
+ return
+ default:
+ return (
+
+
+ This tab will provide an abstracted interface for editing the
+ configuration in Consul, Vault, AWS SSM Parameter Store, and K8s
+ Configuration Maps.
+
+
+ )
+ }
+}
+
+Configuration.propTypes = {
+ urlPath: PropTypes.string.isRequired,
+ project: PropTypes.object.isRequired
+}
+export { Configuration }
diff --git a/src/js/views/Project/Configuration/DisplaySSMParam.jsx b/src/js/views/Project/Configuration/DisplaySSMParam.jsx
new file mode 100644
index 00000000..2f40ee59
--- /dev/null
+++ b/src/js/views/Project/Configuration/DisplaySSMParam.jsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+import { DescriptionList } from '../../../components/DescriptionList/DescriptionList'
+import { Definition } from '../../../components/DescriptionList/Definition'
+
+function DisplaySSMParam({ param }) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+ {param.name}
+ {param.type}
+
+ Values
+
+ {param.values.map(({ environment, value }, i) => {
+ return (
+
+ {value}
+
+ )
+ })}
+
+ >
+ )
+}
+
+DisplaySSMParam.propTypes = {
+ param: PropTypes.object.isRequired
+}
+
+export { DisplaySSMParam }
diff --git a/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx b/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx
new file mode 100644
index 00000000..e0bd7b6d
--- /dev/null
+++ b/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx
@@ -0,0 +1,47 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+import { DescriptionList } from '../../../components/DescriptionList/DescriptionList'
+import { Definition } from '../../../components/DescriptionList/Definition'
+import { Toggle } from '../../../components/Form/Toggle'
+
+function DisplaySecureStringParam({ param }) {
+ const { t } = useTranslation()
+ const [isShown, setIsShown] = useState(false)
+
+ return (
+ <>
+
+ {param.name}
+ {param.type}
+
+
+
Values
+
+
Show decrypted value
+
setIsShown(value)}
+ name="is-hidden"
+ value={isShown}
+ />
+
+
+
+
+ {param.values.map(({ environment, value }, i) => {
+ return (
+
+ {isShown ? value : '********'}
+
+ )
+ })}
+
+ >
+ )
+}
+
+DisplaySecureStringParam.propTypes = {
+ param: PropTypes.object.isRequired
+}
+
+export { DisplaySecureStringParam }
diff --git a/src/js/views/Project/Configuration/SSMConfiguration.jsx b/src/js/views/Project/Configuration/SSMConfiguration.jsx
new file mode 100644
index 00000000..36ea2bf3
--- /dev/null
+++ b/src/js/views/Project/Configuration/SSMConfiguration.jsx
@@ -0,0 +1,180 @@
+import PropTypes from 'prop-types'
+import React, { useContext, useEffect, useRef, useState } from 'react'
+
+import { Context } from '../../../state'
+import { Alert, Icon, Loading, Table } from '../../../components'
+import { useTranslation } from 'react-i18next'
+import { SlideOver } from '../../../components/SlideOver/SlideOver'
+import { ViewSSMParam } from './ViewSSMParam'
+import { useSearchParams } from 'react-router-dom'
+import { httpGet } from '../../../utils'
+
+function cloneParams(searchParams) {
+ const newParams = new URLSearchParams()
+ for (const [key, value] of searchParams) {
+ newParams.set(key, value)
+ }
+ return newParams
+}
+
+function SSMConfiguration({ project }) {
+ const [globalState, dispatch] = useContext(Context)
+ const { t } = useTranslation()
+ const [searchParams, setSearchParams] = useSearchParams()
+ const [onFetch, setOnFetch] = useState(true)
+ const [fetching, setFetching] = useState(false)
+ const [errorMessage, setErrorMessage] = useState()
+ const [rows, setRows] = useState([])
+ const [slideOverOpen, setSlideOverOpen] = useState(false)
+ const [selectedIndex, setSelectedIndex] = useState()
+ const [slideOverFocusTrigger, setSlideOverFocusTrigger] = useState({})
+ const [showArrows, setShowArrows] = useState(false)
+ const arrowLeftRef = useRef(null)
+ const arrowRightRef = useRef(null)
+
+ if (searchParams.get('v') && !slideOverOpen && rows.length > 0) {
+ setSlideOverOpen(true)
+ setShowArrows(true)
+ setSelectedIndex(
+ rows.findIndex((row) => row.name === searchParams.get('v'))
+ )
+ } else if (!searchParams.get('v') && slideOverOpen) {
+ setSlideOverOpen(false)
+ setShowArrows(false)
+ }
+
+ function move(index) {
+ setSelectedIndex(index)
+ const newParams = cloneParams(searchParams)
+ newParams.set('v', rows[index].name)
+ setSearchParams(newParams)
+ }
+
+ useEffect(() => {
+ if (fetching || !onFetch) return
+ setFetching(true)
+
+ httpGet(
+ globalState.fetch,
+ new URL(`/projects/${project.id}/configuration/ssm`, globalState.baseURL),
+ ({ data }) => {
+ setRows(data.sort((a, b) => (a.name > b.name ? 1 : -1)))
+ setFetching(false)
+ },
+ (message) => {
+ setErrorMessage(message)
+ setFetching(false)
+ }
+ )
+ setOnFetch(false)
+ }, [onFetch])
+
+ if (fetching) return
+ if (errorMessage) return {errorMessage}
+
+ return (
+
+
{
+ const newParams = cloneParams(searchParams)
+ newParams.set('v', rows[index].name)
+ setSearchParams(newParams)
+ setSlideOverOpen(true)
+ setSelectedIndex(index)
+ setShowArrows(true)
+ }}
+ checkIsHighlighted={(row) => row.name === searchParams.get('v')}
+ />
+
+ {t('aws.ssmParameter')}
+ {selectedIndex !== undefined && showArrows && (
+ <>
+ {
+ if (selectedIndex > 0) move(selectedIndex - 1)
+ }}
+ tabIndex={selectedIndex === 0 ? -1 : 0}>
+
+
+
+ {
+ if (selectedIndex < rows.length - 1) move(selectedIndex + 1)
+ }}
+ tabIndex={selectedIndex === rows.length - 1 ? -1 : 0}>
+
+
+ >
+ )}
+
+ }
+ focusTrigger={slideOverFocusTrigger}
+ onClose={() => {
+ const newParams = cloneParams(searchParams)
+ newParams.delete('v')
+ setSearchParams(newParams)
+ setSlideOverOpen(false)
+ }}
+ onKeyDown={(e) => {
+ if (!showArrows) return
+ if (selectedIndex > 0 && e.key === 'ArrowLeft') {
+ arrowLeftRef.current?.focus()
+ move(selectedIndex - 1)
+ } else if (
+ selectedIndex < rows.length - 1 &&
+ e.key === 'ArrowRight'
+ ) {
+ arrowRightRef.current?.focus()
+ move(selectedIndex + 1)
+ }
+ }}>
+
+
+
+ )
+}
+
+SSMConfiguration.propTypes = {
+ project: PropTypes.object.isRequired
+}
+export { SSMConfiguration }
diff --git a/src/js/views/Project/Configuration/ViewSSMParam.jsx b/src/js/views/Project/Configuration/ViewSSMParam.jsx
new file mode 100644
index 00000000..6ed6105b
--- /dev/null
+++ b/src/js/views/Project/Configuration/ViewSSMParam.jsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { DisplaySSMParam } from './DisplaySSMParam'
+import { DisplaySecureStringParam } from './DisplaySecureStringParam'
+
+function ViewSSMParam({ param }) {
+ if (param.type === 'SecureString') {
+ return
+ } else {
+ return
+ }
+}
+
+ViewSSMParam.propTypes = {
+ param: PropTypes.object
+}
+
+export { ViewSSMParam }
diff --git a/src/js/views/Project/Project.jsx b/src/js/views/Project/Project.jsx
index 485e67b5..e3e9c403 100644
--- a/src/js/views/Project/Project.jsx
+++ b/src/js/views/Project/Project.jsx
@@ -21,13 +21,13 @@ import {
Tooltip
} from '../../components'
-import { Configuration } from './Configuration'
import { Dependencies } from './Dependencies'
import { Logs } from './Logs'
import { Notes } from './Notes'
import { Overview } from './Overview'
import { Settings } from './Settings'
import { OperationsLog } from '../OperationsLog'
+import { Configuration } from './Configuration/Configuration'
function ProjectPage({ project, factTypes, refresh }) {
const [state, dispatch] = useContext(Context)
@@ -132,7 +132,7 @@ function ProjectPage({ project, factTypes, refresh }) {
/>
}
+ element={ }
/>