diff --git a/locales/en/common.json b/locales/en/common.json index 0cc04be22..5312a01b8 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -99,6 +99,8 @@ "TIMEZONE": "Timezone", "TO": "To", "UNKNOWN_ERROR": "Unknown error", + "UPDATE": "Update", + "UPDATING": "Updating", "USER_SUBMITTED": "User-submitted", "VIEW": "View", "VIEW_MORE": "View more", diff --git a/src/app/CreateRecording/utils.ts b/src/app/CreateRecording/utils.ts index b04dab30c..18a7d233a 100644 --- a/src/app/CreateRecording/utils.ts +++ b/src/app/CreateRecording/utils.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { TemplateType } from '@app/Shared/Services/api.types'; +import { EventTemplateIdentifier } from './types'; + export const RecordingNamePattern = /^[\w_]+$/; export const DurationPattern = /^[1-9][0-9]*$/; @@ -20,3 +23,17 @@ export const DurationPattern = /^[1-9][0-9]*$/; export const isRecordingNameValid = (name: string) => RecordingNamePattern.test(name); export const isDurationValid = (duration: number) => DurationPattern.test(`${duration}`); + +export const templateFromEventSpecifier = (eventSpecifier: string): EventTemplateIdentifier | undefined => { + const regex = /^template=([a-zA-Z0-9]+)(?:,type=([a-zA-Z0-9]+))?$/im; + if (eventSpecifier && regex.test(eventSpecifier)) { + const parts = regex.exec(eventSpecifier); + if (parts) { + return { + name: parts[1], + type: parts[2] as TemplateType, + }; + } + } + return undefined; +}; diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 585a78494..024827427 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -41,7 +41,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { useDayjs } from '@app/utils/hooks/useDayjs'; import { useSort } from '@app/utils/hooks/useSort'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { formatBytes, formatDuration, LABEL_TEXT_MAXWIDTH, sortResources, TableColumn } from '@app/utils/utils'; +import { formatBytes, formatDuration, LABEL_TEXT_MAXWIDTH, sortResources, TableColumn, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Button, @@ -190,7 +190,7 @@ export const ActiveRecordingsTable: React.FC = (prop ); const handleCreateRecording = React.useCallback(() => { - navigate('create', { relative: 'path' }); + navigate(toPath('/recordings/create'), { relative: 'path' }); }, [navigate]); const handleEditLabels = React.useCallback(() => { diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index dc448ff16..b69b37ec1 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -16,6 +16,7 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { BreadcrumbTrail } from '@app/BreadcrumbPage/types'; import { EventTemplateIdentifier } from '@app/CreateRecording/types'; +import { templateFromEventSpecifier } from '@app/CreateRecording/utils'; import { MatchExpressionHint } from '@app/Shared/Components/MatchExpression/MatchExpressionHint'; import { MatchExpressionVisualizer } from '@app/Shared/Components/MatchExpression/MatchExpressionVisualizer'; import { SelectTemplateSelectorForm } from '@app/Shared/Components/SelectTemplateSelectorForm'; @@ -58,7 +59,7 @@ import { HelpIcon } from '@patternfly/react-icons'; import _ from 'lodash'; import * as React from 'react'; import { Trans } from 'react-i18next'; -import { useNavigate } from 'react-router-dom-v5-compat'; +import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; import { combineLatest, forkJoin, iif, of, Subject } from 'rxjs'; import { catchError, debounceTime, map, switchMap, tap } from 'rxjs/operators'; import { RuleFormData } from './types'; @@ -70,11 +71,13 @@ export const CreateRuleForm: React.FC = (_props) => { const context = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); const navigate = useNavigate(); + const location = useLocation(); const { t } = useCryostatTranslation(); // Do not use useSearchExpression for display. This causes the cursor to jump to the end due to async updates. const matchExprService = useMatchExpressionSvc(); const addSubscription = useSubscriptions(); const [autoanalyze, setAutoanalyze] = React.useState(true); + const [isExistingEdit, setExistingEdit] = React.useState(false); const [formData, setFormData] = React.useState({ name: '', @@ -100,6 +103,10 @@ export const CreateRuleForm: React.FC = (_props) => { const matchedTargetsRef = React.useRef(new Subject()); + React.useEffect(() => { + setExistingEdit(location?.state?.edit ?? false); + }, [location, setExistingEdit]); + const eventSpecifierString = React.useMemo(() => { let str = ''; const { template } = formData; @@ -157,6 +164,50 @@ export const CreateRuleForm: React.FC = (_props) => { [setFormData, matchExprService], ); + React.useEffect(() => { + const prefilled: Partial = location.state || {}; + const eventSpecifier = location?.state?.eventSpecifier; + const maxAgeSeconds = location?.state?.maxAgeSeconds; + const maxSizeBytes = location?.state?.maxSizeBytes; + const archivalPeriodSeconds = location?.state?.archivalPeriodSeconds; + let { + name, + enabled, + description, + matchExpression, + maxAge, + maxAgeUnit, + maxSize, + maxSizeUnit, + archivalPeriod, + archivalPeriodUnit, + initialDelay, + initialDelayUnit, + preservedArchives, + } = prefilled; + setFormData((old) => ({ + ...old, + enabled: enabled ?? true, + name: name ?? '', + description: description ?? '', + matchExpression: matchExpression ?? '', + template: templateFromEventSpecifier(eventSpecifier), + maxAge: maxAgeSeconds ?? maxAge ?? 0, + maxAgeUnit: maxAgeSeconds ? 1 : (maxAgeUnit ?? 1), + maxSize: maxSizeBytes ?? maxSize ?? 0, + maxSizeUnit: maxSizeBytes ? 1 : (maxSizeUnit ?? 1), + archivalPeriod: archivalPeriodSeconds ?? archivalPeriod ?? 0, + archivalPeriodUnit: archivalPeriodSeconds ? 1 : (archivalPeriodUnit ?? 1), + initialDelay: initialDelay ?? 0, + initialDelayUnit: initialDelayUnit ?? 1, + preservedArchives: preservedArchives ?? 0, + })); + handleNameChange(null, name ?? ''); + if (matchExpression) { + handleMatchExpressionChange(null, matchExpression); + } + }, [location, setFormData, handleNameChange, handleMatchExpressionChange]); + const handleTemplateChange = React.useCallback( (template: EventTemplateIdentifier) => setFormData((old) => ({ ...old, template })), [setFormData], @@ -269,14 +320,24 @@ export const CreateRuleForm: React.FC = (_props) => { }; setLoading(true); addSubscription( - context.api.createRule(rule).subscribe((success) => { + (isExistingEdit ? context.api.updateRule(rule, true) : context.api.createRule(rule)).subscribe((success) => { setLoading(false); if (success) { exitForm(); } }), ); - }, [setLoading, addSubscription, exitForm, context.api, notifications, formData, autoanalyze, eventSpecifierString]); + }, [ + setLoading, + addSubscription, + exitForm, + context.api, + notifications, + formData, + autoanalyze, + eventSpecifierString, + isExistingEdit, + ]); React.useEffect(() => { const matchedTargets = matchedTargetsRef.current; @@ -360,7 +421,7 @@ export const CreateRuleForm: React.FC = (_props) => { = (_props) => { data-quickstart-id="rule-create-btn" {...createButtonLoadingProps} > - {t(loading ? 'CREATING' : 'CREATE')} + {t(isExistingEdit ? (loading ? 'UPDATING' : 'UPDATE') : loading ? 'CREATING' : 'CREATE')}