Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impl [Project][Secrets] Add new Secrets table with validation #2437

Merged
merged 3 commits into from
May 9, 2024
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"final-form-arrays": "^3.1.0",
"fs-extra": "^10.0.0",
"identity-obj-proxy": "^3.0.0",
"iguazio.dashboard-react-controls": "2.0.4",
"iguazio.dashboard-react-controls": "2.0.5",
"is-wsl": "^1.1.0",
"js-base64": "^2.5.2",
"js-yaml": "^4.1.0",
Expand Down
5 changes: 0 additions & 5 deletions src/actions/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ import {
FETCH_PROJECT_SECRETS_BEGIN,
FETCH_PROJECT_SECRETS_FAILURE,
FETCH_PROJECT_SECRETS_SUCCESS,
SET_PROJECT_SECRETS,
SET_JOBS_MONITORING_DATA,
SET_MLRUN_IS_UNHEALTHY,
SET_MLRUN_UNHEALTHY_RETRYING
Expand Down Expand Up @@ -623,10 +622,6 @@ const projectsAction = {
setJobsMonitoringData: data => ({
type: SET_JOBS_MONITORING_DATA,
payload: data
}),
setProjectSecrets: secrets => ({
type: SET_PROJECT_SECRETS,
payload: secrets
})
}

Expand Down
1 change: 0 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,6 @@ export const FETCH_PROJECT_DATASETS_FAILURE = 'FETCH_PROJECT_DATASETS_FAILURE'
export const FETCH_PROJECT_SUMMARY_BEGIN = 'FETCH_PROJECT_SUMMARY_BEGIN'
export const FETCH_PROJECT_SUMMARY_FAILURE = 'FETCH_PROJECT_SUMMARY_FAILURE'
export const FETCH_PROJECT_SUMMARY_SUCCESS = 'FETCH_PROJECT_SUMMARY_SUCCESS'
export const SET_PROJECT_SECRETS = 'SET_PROJECT_SECRETS'
export const FETCH_PROJECT_DATASETS_SUCCESS = 'FETCH_PROJECT_DATASETS_SUCCESS'
export const FETCH_PROJECT_FAILED_JOBS_BEGIN = 'FETCH_PROJECT_FAILED_JOBS_BEGIN'
export const FETCH_PROJECT_FAILED_JOBS_FAILURE = 'FETCH_PROJECT_FAILED_JOBS_FAILURE'
Expand Down
203 changes: 134 additions & 69 deletions src/elements/ProjectSettingsSecrets/ProjectSettingsSecrets.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,49 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Form } from 'react-final-form'
import { connect, useDispatch } from 'react-redux'
import { createForm } from 'final-form'
import { differenceWith, isEmpty, isEqual } from 'lodash'
import { useParams } from 'react-router-dom'
import arrayMutators from 'final-form-arrays'

import Loader from '../../common/Loader/Loader'
import { FormKeyValueTable } from 'igz-controls/components'

import {
ADD_PROJECT_SECRET,
DELETE_PROJECT_SECRET,
EDIT_PROJECT_SECRET
} from './ProjectSettingsSecrets.utils'
import ProjectSettingsSecretsView from './ProjectSettingsSecretsView'
import { areFormValuesChanged, setFieldState } from 'igz-controls/utils/form.util'
import projectApi from '../../api/projects-api'
import projectsAction from '../../actions/projects'
import { FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants'
import { getErrorMsg } from 'igz-controls/utils/common.util'
import { getValidationRules } from 'igz-controls/utils/validation.util'
import { showErrorNotification } from '../../utils/notifications.util'

const ProjectSettingsSecrets = ({
fetchProjectSecrets,
projectStore,
removeProjectData,
setNotification,
setProjectSecrets
setNotification
}) => {
const [modifyingIsInProgress, setModifyingIsInProgress] = useState(false)
const [lastEditedFormValues, setLastEditedFormValues] = useState({})
const [isUserAllowed, setIsUserAllowed] = useState(true)
const params = useParams()
const dispatch = useDispatch()
const formRef = React.useRef(
createForm({
initialValues: {},
mutators: { ...arrayMutators, setFieldState },
onSubmit: () => {}
})
)
const formStateRef = useRef(null)

const fetchSecrets = useCallback(() => {
setIsUserAllowed(true)
Expand All @@ -66,97 +83,145 @@ const ProjectSettingsSecrets = ({
}
}, [fetchSecrets, removeProjectData, params.projectName])

const generalSecrets = useMemo(
() =>
projectStore.project.secrets?.data['secret_keys']
? projectStore.project.secrets.data['secret_keys'].map(secret => ({
key: secret,
value: '*****'
}))
: [],
[projectStore.project.secrets.data]
)
useEffect(() => {
const formSecrets = projectStore.project.secrets?.data['secret_keys']
? projectStore.project.secrets.data['secret_keys'].map(secret => ({
data: {
key: secret,
value: ''
}
}))
: []
const newInitial = {
secrets: formSecrets
}

setLastEditedFormValues(newInitial)
formStateRef.current.form.restart(newInitial)
}, [projectStore.project.secrets.data])

const handleProjectSecret = useCallback(
(type, data) => {
const modifyProjectSecret = useCallback(
(modificationType, requestData) => {
setModifyingIsInProgress(true)
const updateSecret =
type === ADD_PROJECT_SECRET || type === EDIT_PROJECT_SECRET
modificationType === ADD_PROJECT_SECRET || modificationType === EDIT_PROJECT_SECRET
? projectApi.setProjectSecret
: projectApi.deleteSecret

updateSecret(params.projectName, data)
updateSecret(params.projectName, requestData)
.then(() => {
dispatch(
setNotification({
status: 200,
id: Math.random(),
message: `Secret ${
type === DELETE_PROJECT_SECRET
modificationType === DELETE_PROJECT_SECRET
? 'deleted'
: type === EDIT_PROJECT_SECRET
? 'edited'
: 'added'
: modificationType === EDIT_PROJECT_SECRET
? 'edited'
: 'added'
} successfully`
})
)
})
.catch(error => {
showErrorNotification(dispatch, error, 'Failed to update secrets')
fetchSecrets()
})
.finally(() => setModifyingIsInProgress(false))
},
[dispatch, params.projectName, setNotification]
[dispatch, fetchSecrets, params.projectName, setNotification]
)

const handleAddNewSecret = useCallback(
createSecretData => {
const data = {
provider: 'kubernetes',
secrets: {
[createSecretData.key]: createSecretData.value
const updateSecretsData = useCallback(() => {
setTimeout(() => {
const formStateLocal = formStateRef.current

if (
areFormValuesChanged(lastEditedFormValues, formStateLocal.values) &&
formStateLocal.valid
) {
const modificationType =
formStateLocal.values.secrets.length > lastEditedFormValues.secrets.length
? ADD_PROJECT_SECRET
: formStateLocal.values.secrets.length === lastEditedFormValues.secrets.length
? EDIT_PROJECT_SECRET
: DELETE_PROJECT_SECRET
const primarySecretsArray =
modificationType === DELETE_PROJECT_SECRET
? lastEditedFormValues.secrets
: formStateLocal.values.secrets
const secondarySecretsArray =
modificationType === DELETE_PROJECT_SECRET
? formStateLocal.values.secrets
: lastEditedFormValues.secrets
const differences = differenceWith(primarySecretsArray, secondarySecretsArray, isEqual)

if (!isEmpty(differences)) {
const changedData = differences[0].data
const newSecrets = formStateLocal.values.secrets.map(secretData => ({
data: { key: secretData.data.key, value: '' }
}))
const newFormValues = { secrets: newSecrets }
const requestData =
modificationType === DELETE_PROJECT_SECRET
? changedData.key
: { provider: 'kubernetes', secrets: { [changedData.key]: changedData.value } }

setLastEditedFormValues(newFormValues)
formStateRef.current.form.restart(newFormValues)
modifyProjectSecret(modificationType , requestData)
}
}

const secretKeys = [
...(projectStore.project.secrets.data?.secret_keys ?? []),
createSecretData.key
]

setProjectSecrets(secretKeys) // redux
handleProjectSecret(ADD_PROJECT_SECRET, data) // api
},
[handleProjectSecret, projectStore.project.secrets.data, setProjectSecrets]
)

const handleSecretDelete = (index, secret) => {
const filteredArray = projectStore.project.secrets?.data['secret_keys'].filter(
(_, elementIndex) => elementIndex !== index
)

setProjectSecrets(filteredArray)
handleProjectSecret(DELETE_PROJECT_SECRET, secret.key) // api
}

const handleSecretEdit = editedSecretData => {
const data = {
provider: 'kubernetes',
secrets: {
[editedSecretData.key]: editedSecretData.value
}
}

handleProjectSecret(EDIT_PROJECT_SECRET, data) // api
}
})
}, [modifyProjectSecret, lastEditedFormValues])

return (
<ProjectSettingsSecretsView
error={projectStore.project.secrets?.error}
handleAddNewSecret={handleAddNewSecret}
handleSecretDelete={handleSecretDelete}
handleSecretEdit={handleSecretEdit}
isUserAllowed={isUserAllowed}
loading={projectStore.project.secrets?.loading}
secrets={generalSecrets}
/>
<Form form={formRef.current} onSubmit={() => {}}>
{formState => {
formStateRef.current = formState

return (
<div className="settings__card">
{projectStore.project.secrets?.loading ? (
<Loader />
) : !isUserAllowed ? (
<div>
<h1>You don't have access to this project's secrets</h1>
</div>
) : (
<div className="settings__card-content">
<div className="settings__card-content-col">
<p className="settings__card-subtitle">
These secrets are automatically available to all jobs belonging to this project
that are not executed locally. See{' '}
<a
href="https://docs.mlrun.org/en/latest/secrets.html"
target="_blank"
rel="noopener noreferrer"
className="link"
>
Secrets
</a>
</p>
<FormKeyValueTable
addNewItemLabel="Add secret"
isKeyEditable={false}
isValuePassword={true}
valueType="password"
disabled={modifyingIsInProgress}
keyValidationRules={getValidationRules('project.secrets.key')}
onExitEditModeCallback={updateSecretsData}
fieldsPath="secrets"
formState={formState}
/>
</div>
</div>
)}
</div>
)
}}
</Form>
)
}

Expand Down
12 changes: 0 additions & 12 deletions src/elements/ProjectSettingsSecrets/ProjectSettingsSecrets.scss

This file was deleted.

Loading
Loading