Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 215 additions & 2 deletions src/containers/apps/ProjectDetailsEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Button, Card, Input, Row } from 'antd'
import { EditFilled, EditOutlined } from '@ant-design/icons'
import { Button, Card, Col, Input, Row, Tooltip } from 'antd'
import { RefObject } from 'react'
import { connect } from 'react-redux'
import { RouteComponentProps } from 'react-router'
import ProjectSelector from '../../components/ProjectSelector'
import { IMobileComponent } from '../../models/ContainerProps'
import { IHashMapGeneric } from '../../models/IHashMapGeneric'
import ProjectDefinition from '../../models/ProjectDefinition'
import { localize } from '../../utils/Language'
import Toaster from '../../utils/Toaster'
import Utils from '../../utils/Utils'
import ApiComponent from '../global/ApiComponent'
import CenteredSpinner from '../global/CenteredSpinner'
import CodeEdit from '../global/CodeEdit'
import ErrorRetry from '../global/ErrorRetry'
import { IAppEnvVar } from './AppDefinition'

interface PropsInterface extends RouteComponentProps<any> {
mainContainer: RefObject<HTMLDivElement>
Expand All @@ -25,6 +29,8 @@ class ProjectDetailsEdit extends ApiComponent<
isLoading: boolean
selectedProject: ProjectDefinition | undefined
allProjects: ProjectDefinition[]
envVarBulkEdit: boolean
envVarBulkVals: string
}
> {
constructor(props: any) {
Expand All @@ -34,9 +40,197 @@ class ProjectDetailsEdit extends ApiComponent<
isLoading: true,
selectedProject: undefined,
allProjects: [],
envVarBulkEdit: false,
envVarBulkVals: '',
}
}

parseEnvVars(src: string): IHashMapGeneric<string> {
const obj: IHashMapGeneric<string> = {}
src.toString()
.split('\n')
.forEach(function (line) {
const keyValueArr = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/)
if (!!keyValueArr) {
const key = keyValueArr[1]
let value = keyValueArr[2] || ''
const len = value ? value.length : 0
if (
len > 0 &&
value.charAt(0) === '"' &&
value.charAt(len - 1) === '"'
) {
value = value.replace(/\\n/gm, '\n')
}
value = value.replace(/(^['"]|['"]$)/g, '').trim()
obj[key] = value
}
})
return obj
}

convertEnvVarsToBulk(envVars: IAppEnvVar[]): string {
return envVars
.map((e) => {
let val = e.value
if (val.indexOf('\n') >= 0) {
val = `"${val.split('\n').join('\\n')}"`
}
return `${e.key}=${val}`
})
.join('\n')
}

createEnvVarSection() {
const self = this
const selectedProject = self.state.selectedProject!
const envVars = selectedProject.envVars || []

const updateEnvVars = (updated: IAppEnvVar[]) => {
const newData = Utils.copyObject(selectedProject)
newData.envVars = updated
self.setState({ selectedProject: newData })
}

const toggleIcon = self.state.envVarBulkEdit ? (
<Tooltip title="Switch to key/value editor">
<EditFilled
style={{ cursor: 'pointer' }}
onClick={() =>
self.setState({
envVarBulkEdit: false,
envVarBulkVals: '',
})
}
/>
</Tooltip>
) : (
<Tooltip title="Switch to bulk editor">
<EditOutlined
style={{ cursor: 'pointer' }}
onClick={() =>
self.setState({
envVarBulkEdit: true,
envVarBulkVals:
self.convertEnvVarsToBulk(envVars),
})
}
/>
</Tooltip>
)

if (self.state.envVarBulkEdit) {
return (
<div>
<Row style={{ paddingBottom: 12 }}>
<Col span={24}>
<CodeEdit
placeholder={'key1=value1\nkey2=value2'}
rows={7}
value={
self.state.envVarBulkVals
? self.state.envVarBulkVals
: self.convertEnvVarsToBulk(envVars)
}
onChange={(e) => {
const keyVals = self.parseEnvVars(
e.target.value
)
const parsed: IAppEnvVar[] = []
Object.keys(keyVals).forEach((k) => {
parsed.push({
key: k,
value: keyVals[k],
})
})
updateEnvVars(parsed)
self.setState({
envVarBulkVals: e.target.value,
})
}}
/>
</Col>
</Row>
<Row justify="end">{toggleIcon}</Row>
</div>
)
}

const rows = envVars.map((value, index) => (
<Row style={{ paddingBottom: 12 }} key={`${index}`}>
<Col span={8}>
<Input
spellCheck={false}
autoCorrect="off"
autoComplete="off"
autoCapitalize="off"
className="code-input"
placeholder="key"
value={value.key}
type="text"
onChange={(e) => {
const updated = Utils.copyObject(envVars)
updated[index].key = e.target.value
updateEnvVars(updated)
}}
/>
</Col>
<Col style={{ paddingLeft: 12 }} span={13}>
<Input
spellCheck={false}
autoCorrect="off"
autoComplete="off"
autoCapitalize="off"
className="code-input"
placeholder="value"
value={value.value}
type="text"
onChange={(e) => {
const updated = Utils.copyObject(envVars)
updated[index].value = e.target.value
updateEnvVars(updated)
}}
/>
</Col>
<Col style={{ paddingLeft: 12 }} span={3}>
<Button
danger
onClick={() => {
const updated = Utils.copyObject(envVars)
updated.splice(index, 1)
updateEnvVars(updated)
}}
>
</Button>
</Col>
</Row>
))

return (
<div>
{rows}
<Row style={{ paddingTop: 8 }} justify="space-between">
<Button
type="default"
onClick={() => {
updateEnvVars([
...envVars,
{ key: '', value: '' },
])
}}
>
{localize(
'projects.add_env_var',
'Add Key/Value Pair'
)}
</Button>
{toggleIcon}
</Row>
</div>
)
}

goBackToApps() {
this.props.history.push('/apps')
}
Expand Down Expand Up @@ -67,7 +261,7 @@ class ProjectDetailsEdit extends ApiComponent<
<p>
{localize(
'projects.edit_project_hint',
'You can set the name, description and the parent of this project.'
'You can set the name, description, parent, and environment variables for this project.'
)}
</p>
<div style={{ height: 20 }} />
Expand Down Expand Up @@ -158,6 +352,24 @@ class ProjectDetailsEdit extends ApiComponent<
})
}}
/>
<div
style={{
marginTop: 32,
marginBottom: 5,
}}
>
{localize(
'projects.env_vars',
'Environment Variables'
)}
</div>
<p style={{ color: '#888', fontSize: 12 }}>
{localize(
'projects.env_vars_hint',
'Apps and sub-projects inside this project will inherit these variables. Apps can override them by defining their own variable with the same key.'
)}
</p>
{self.createEnvVarSection()}
<Row style={{ marginTop: 48 }} justify="end">
<Button
style={{ marginInlineEnd: 20 }}
Expand Down Expand Up @@ -241,6 +453,7 @@ class ProjectDetailsEdit extends ApiComponent<
id: '',
name: '',
description: '',
envVars: [],
},
allProjects: projects,
})
Expand Down
108 changes: 107 additions & 1 deletion src/containers/apps/appDetails/AppConfigs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { EditFilled, EditOutlined, InfoCircleOutlined } from '@ant-design/icons'
import { Button, Col, Input, Row, Switch, Tag, Tooltip } from 'antd'
import { Button, Col, Input, Row, Switch, Tag, Tooltip, Typography } from 'antd'
import { Component, Fragment } from 'react'
import { IHashMapGeneric } from '../../../models/IHashMapGeneric'
import ProjectDefinition from '../../../models/ProjectDefinition'
import { localize } from '../../../utils/Language'
import Utils from '../../../utils/Utils'
import CodeEdit from '../../global/CodeEdit'
Expand Down Expand Up @@ -69,6 +70,110 @@ export default class AppConfigs extends Component<
return obj
}

getInheritedEnvVars(): { envVars: IAppEnvVar[]; projectName: string }[] {
const app = this.props.apiData.appDefinition
const projects = this.props.apiData.projects || []

if (!app.projectId) return []

const projectMap = new Map<string, ProjectDefinition>()
projects.forEach((p) => projectMap.set(p.id, p))

const chain: ProjectDefinition[] = []
let current = projectMap.get(app.projectId)
while (current) {
chain.unshift(current)
current = current.parentProjectId
? projectMap.get(current.parentProjectId)
: undefined
}

return chain
.filter((p) => p.envVars && p.envVars.length > 0)
.map((p) => ({ envVars: p.envVars!, projectName: p.name }))
}

getEffectiveInheritedEnvVars(): IAppEnvVar[] {
const layers = this.getInheritedEnvVars()
const map = new Map<string, string>()
layers.forEach(({ envVars }) =>
envVars.forEach((v) => map.set(v.key, v.value))
)
return Array.from(map.entries()).map(([key, value]) => ({ key, value }))
}

createInheritedEnvVarSection() {
const inherited = this.getEffectiveInheritedEnvVars()
const appEnvVarKeys = new Set(
(this.props.apiData.appDefinition.envVars || []).map((v) => v.key)
)

if (inherited.length === 0) return null

return (
<div
style={{
marginBottom: 16,
padding: '12px 16px',
background: '#f6f6f6',
borderRadius: 6,
border: '1px solid #e0e0e0',
}}
>
<Row align="middle" style={{ marginBottom: 8 }}>
<Typography.Text strong>
{localize(
'apps.inherited_env_vars_title',
'Inherited from project'
)}
</Typography.Text>
<Tooltip
title={localize(
'apps.inherited_env_vars_hint',
'These environment variables are inherited from the parent project(s). To override a variable, add it below with the same key.'
)}
>
<InfoCircleOutlined
style={{ marginLeft: 8, color: '#888' }}
/>
</Tooltip>
</Row>
{inherited.map((v) => (
<Row
key={v.key}
style={{ paddingBottom: 6 }}
align="middle"
>
<Col span={8}>
<Input
className="code-input"
value={v.key}
disabled
/>
</Col>
<Col style={{ paddingLeft: 12 }} span={12}>
<Input
className="code-input"
value={v.value}
disabled
/>
</Col>
<Col style={{ paddingLeft: 12 }} span={4}>
{appEnvVarKeys.has(v.key) && (
<Tag color="blue">
{localize(
'apps.inherited_env_var_overridden',
'overridden'
)}
</Tag>
)}
</Col>
</Row>
))}
</div>
)
}

convertEnvVarsToBulk(envVars: IAppEnvVar[]) {
return envVars
.map((e) => {
Expand Down Expand Up @@ -518,6 +623,7 @@ export default class AppConfigs extends Component<
</h5>
</Col>
</Row>
{this.createInheritedEnvVarSection()}
<div
className={
app.envVars && !!app.envVars.length
Expand Down
Loading