diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1c3a676..274add7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [3.4.6] - 2023-02-01 + +### Added + +* ARGO-4103 POEM POST service types with tags=poem field set + +### Fixed + +* ARGO-4104 Reflect changing number of entries in pagination on rendering of tuples +* ARGO-4160 Form validation triggered wrong +* ARGO-4179 Fix duplicated tuple logic clear off on metric profiles + +### Changed + +* ARGO-4030 Switch aggregation profile page to react-hook-form library +* ARGO-4033 Switch metric overrides page to react-hook-form library +* ARGO-4034 Switch metric profile page to react-hook-form library +* ARGO-4035 Switch metric and metric templates page to react-hook-form library +* ARGO-4036 Switch metric tags page to react-hook-form library +* ARGO-4038 Remove formik from operations profile page +* ARGO-4040 Switch probe page to react-hook-form library +* ARGO-4043 Switch users page to react-hook-form library +* ARGO-4044 Switch YUM repo page to react-hook-form library +* ARGO-4161 Bump libs + ## [3.4.5] - 2022-11-03 ### Added diff --git a/poem/Poem/api/internal_views/metrics.py b/poem/Poem/api/internal_views/metrics.py index 37b7b6781..d6ea2269b 100644 --- a/poem/Poem/api/internal_views/metrics.py +++ b/poem/Poem/api/internal_views/metrics.py @@ -740,9 +740,13 @@ def put(self, request): "{hostname} {metric} {parameter} {value}".format(**item) ) - conf.globalattribute = json.dumps(global_attrs) - conf.hostattribute = json.dumps(host_attrs) - conf.metricparameter = json.dumps(metric_params) + conf.globalattribute = json.dumps(global_attrs) if \ + len(global_attrs) >= 1 and global_attrs[0] != " " else "" + conf.hostattribute = json.dumps(host_attrs) \ + if len(host_attrs) >= 1 and host_attrs[0] != " " else "" + conf.metricparameter = json.dumps(metric_params) \ + if len(metric_params) >= 1 and metric_params[0] != " " \ + else "" conf.save() diff --git a/poem/Poem/api/internal_views/metrictemplates.py b/poem/Poem/api/internal_views/metrictemplates.py index c28495e5b..dd669a3a2 100644 --- a/poem/Poem/api/internal_views/metrictemplates.py +++ b/poem/Poem/api/internal_views/metrictemplates.py @@ -769,8 +769,14 @@ def get(self, request, name=None): if name: try: tag = admin_models.MetricTags.objects.get(name=name) - serializer = serializers.MetricTagsSerializer(tag) - return Response(serializer.data) + metrics = admin_models.MetricTemplate.objects.filter( + tags__name=tag.name + ) + return Response({ + "id": tag.id, + "name": tag.name, + "metrics": sorted([metric.name for metric in metrics]) + }) except admin_models.MetricTags.DoesNotExist: return Response( @@ -780,8 +786,18 @@ def get(self, request, name=None): else: tags = admin_models.MetricTags.objects.all().order_by('name') - serializer = serializers.MetricTagsSerializer(tags, many=True) - return Response(serializer.data) + data = list() + for tag in tags: + metrics = admin_models.MetricTemplate.objects.filter( + tags__name=tag.name + ) + data.append({ + "id": tag.id, + "name": tag.name, + "metrics": sorted([metric.name for metric in metrics]) + }) + + return Response(data) def post(self, request): if request.tenant.schema_name == get_public_schema_name() and \ @@ -1038,33 +1054,11 @@ def delete(self, request, name): ) -class ListMetricTemplates4Tag(APIView): - authentication_classes = (SessionAuthentication,) - - def get(self, request, tag): - try: - admin_models.MetricTags.objects.get(name=tag) - mts = admin_models.MetricTemplate.objects.filter(tags__name=tag) - - return Response(sorted([mt.name for mt in mts])) - - except admin_models.MetricTags.DoesNotExist: - return Response( - {"detail": "Requested tag not found."}, - status=status.HTTP_404_NOT_FOUND - ) - - class ListPublicMetricTags(ListMetricTags): authentication_classes = () permission_classes = () -class ListPublicMetricTemplates4Tag(ListMetricTemplates4Tag): - authentication_classes = () - permission_classes = () - - class ListDefaultPorts(APIView): authentication_classes = (SessionAuthentication,) diff --git a/poem/Poem/api/serializers.py b/poem/Poem/api/serializers.py index 9408b5b2a..7663fb687 100644 --- a/poem/Poem/api/serializers.py +++ b/poem/Poem/api/serializers.py @@ -54,12 +54,6 @@ class Meta: model = models.ThresholdsProfiles -class MetricTagsSerializer(serializers.ModelSerializer): - class Meta: - model = MetricTags - fields = "__all__" - - class DefaultPortsSerializer(serializers.ModelSerializer): class Meta: model = DefaultPort diff --git a/poem/Poem/api/tests/test_metrics.py b/poem/Poem/api/tests/test_metrics.py index 9b9b021c4..50c540418 100644 --- a/poem/Poem/api/tests/test_metrics.py +++ b/poem/Poem/api/tests/test_metrics.py @@ -3663,6 +3663,45 @@ def test_put_configuration_regular_user(self): ]) ) + def test_put_configuration_with_only_name(self): + data = { + "id": self.configuration1.id, + "name": "local_updated", + "global_attributes": [ + { + "attribute": "", + "value": "" + } + ], + "host_attributes": [ + { + "hostname": "", + "attribute": "", + "value": "" + } + ], + "metric_parameters": [ + { + "hostname": "", + "metric": "", + "parameter": "", + "value": "" + } + ] + } + content, content_type = encode_data(data) + request = self.factory.put(self.url, content, content_type=content_type) + force_authenticate(request, user=self.user) + response = self.view(request) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + conf = poem_models.MetricConfiguration.objects.get( + id=self.configuration1.id + ) + self.assertEqual(conf.name, "local_updated") + self.assertEqual(conf.globalattribute, "") + self.assertEqual(conf.hostattribute, "") + self.assertEqual(conf.metricparameter, "") + def test_put_configuration_with_existing_name_super_user(self): data = { "id": self.configuration1.id, diff --git a/poem/Poem/api/tests/test_metrictemplates.py b/poem/Poem/api/tests/test_metrictemplates.py index b4fc14819..6294778a1 100644 --- a/poem/Poem/api/tests/test_metrictemplates.py +++ b/poem/Poem/api/tests/test_metrictemplates.py @@ -9601,19 +9601,23 @@ def test_get_metric_tags_admin_superuser(self): [ { "id": self.tag2.id, - "name": "deprecated" + "name": "deprecated", + "metrics": [] }, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] }, { "id": self.tag3.id, - "name": "test_tag1" + "name": "test_tag1", + "metrics": ["argo.AMS-Check", "argo.EGI-Connectors-Check"] }, { "id": self.tag4.id, - "name": "test_tag2" + "name": "test_tag2", + "metrics": ["argo.AMS-Check", "org.apel.APEL-Pub"] } ] ) @@ -9627,19 +9631,23 @@ def test_get_metric_tags_admin_regular_user(self): [ { "id": self.tag2.id, - "name": "deprecated" + "name": "deprecated", + "metrics": [] }, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] }, { "id": self.tag3.id, - "name": "test_tag1" + "name": "test_tag1", + "metrics": ["argo.AMS-Check", "argo.EGI-Connectors-Check"] }, { "id": self.tag4.id, - "name": "test_tag2" + "name": "test_tag2", + "metrics": ["argo.AMS-Check", "org.apel.APEL-Pub"] } ] ) @@ -9653,19 +9661,23 @@ def test_get_metric_tags_tenant_superuser(self): [ { "id": self.tag2.id, - "name": "deprecated" + "name": "deprecated", + "metrics": [] }, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] }, { "id": self.tag3.id, - "name": "test_tag1" + "name": "test_tag1", + "metrics": ["argo.AMS-Check", "argo.EGI-Connectors-Check"] }, { "id": self.tag4.id, - "name": "test_tag2" + "name": "test_tag2", + "metrics": ["argo.AMS-Check", "org.apel.APEL-Pub"] } ] ) @@ -9679,19 +9691,23 @@ def test_get_metric_tags_tenant_regular_user(self): [ { "id": self.tag2.id, - "name": "deprecated" + "name": "deprecated", + "metrics": [] }, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] }, { "id": self.tag3.id, - "name": "test_tag1" + "name": "test_tag1", + "metrics": ["argo.AMS-Check", "argo.EGI-Connectors-Check"] }, { "id": self.tag4.id, - "name": "test_tag2" + "name": "test_tag2", + "metrics": ["argo.AMS-Check", "org.apel.APEL-Pub"] } ] ) @@ -9709,7 +9725,8 @@ def test_get_metric_tag_by_name_admin_superuser(self): response.data, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] } ) @@ -9721,7 +9738,8 @@ def test_get_metric_tag_by_name_admin_regular_user(self): response.data, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] } ) @@ -9733,7 +9751,8 @@ def test_get_metric_tag_by_name_tenant_superuser(self): response.data, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] } ) @@ -9745,7 +9764,8 @@ def test_get_metric_tag_by_name_tenant_regular_user(self): response.data, { "id": self.tag1.id, - "name": "internal" + "name": "internal", + "metrics": ["argo.AMS-Check"] } ) @@ -14399,25 +14419,6 @@ def test_delete_nonexisting_metric_tag_tenant_regular_user(self, mock_sync): ) -class ListMetricTemplates4Tag(TenantTestCase): - def setUp(self) -> None: - self.factory = TenantRequestFactory(self.tenant) - self.view = views.ListMetricTemplates4Tag.as_view() - self.url = '/api/v2/internal/metrictags/' - self.user = CustUser.objects.create_user(username='test') - - mock_db() - - def test_get_metrics4tag(self): - request = self.factory.get(self.url + "test_tag2") - force_authenticate(request, user=self.user) - response = self.view(request, "test_tag2") - self.assertEqual( - response.data, - ["argo.AMS-Check", "org.apel.APEL-Pub"] - ) - - class ListDefaultPortsTests(TenantTestCase): def setUp(self): self.factory = TenantRequestFactory(self.tenant) diff --git a/poem/Poem/api/tests/test_yumrepos.py b/poem/Poem/api/tests/test_yumrepos.py index 866dffe60..1d29ffe90 100644 --- a/poem/Poem/api/tests/test_yumrepos.py +++ b/poem/Poem/api/tests/test_yumrepos.py @@ -933,7 +933,7 @@ def test_put_yum_repo_with_nonexisting_tag_tenant_user(self): def test_put_yum_repo_with_nonexisting_repo_sp_superuser(self): data = { - 'id': 999, + 'id': self.repo1.id + self.repo2.id, 'name': 'repo-3', 'tag': 'CentOS 6', 'description': 'New repo description.' @@ -948,7 +948,7 @@ def test_put_yum_repo_with_nonexisting_repo_sp_superuser(self): def test_put_yum_repo_with_nonexisting_repo_sp_user(self): data = { - 'id': 999, + 'id': self.repo1.id + self.repo2.id, 'name': 'repo-3', 'tag': 'CentOS 6', 'description': 'New repo description.' @@ -966,7 +966,7 @@ def test_put_yum_repo_with_nonexisting_repo_sp_user(self): def test_put_yum_repo_with_nonexisting_repo_tenant_superuser(self): data = { - 'id': 999, + 'id': self.repo1.id + self.repo2.id, 'name': 'repo-3', 'tag': 'CentOS 6', 'description': 'New repo description.' @@ -984,7 +984,7 @@ def test_put_yum_repo_with_nonexisting_repo_tenant_superuser(self): def test_put_yum_repo_with_nonexisting_repo_tenant_user(self): data = { - 'id': 999, + 'id': self.repo1.id + self.repo2.id, 'name': 'repo-3', 'tag': 'CentOS 6', 'description': 'New repo description.' diff --git a/poem/Poem/api/urls_internal.py b/poem/Poem/api/urls_internal.py index 240c966a2..7ca2e0ba2 100644 --- a/poem/Poem/api/urls_internal.py +++ b/poem/Poem/api/urls_internal.py @@ -84,8 +84,6 @@ path('metrictags/', views_internal.ListMetricTags.as_view(), name='metrictags'), path('metrictags/', views_internal.ListMetricTags.as_view(), name='metrictags'), path('public_metrictags/', views_internal.ListPublicMetricTags.as_view(), name='metrictags'), - path("metrics4tags/", views_internal.ListMetricTemplates4Tag.as_view(), name="metrics4tags"), - path("public_metrics4tags/", views_internal.ListPublicMetricTemplates4Tag.as_view(), name="metrics4tags"), path('reports/', views_internal.ListReports.as_view(), name='reports'), path('public_reports/', views_internal.ListPublicReports.as_view(), name='reports'), path('reports/', views_internal.ListReports.as_view(), name='reports'), diff --git a/poem/Poem/frontend/react/AggregationProfiles.js b/poem/Poem/frontend/react/AggregationProfiles.js index 25482ae01..a5ef124b8 100644 --- a/poem/Poem/frontend/react/AggregationProfiles.js +++ b/poem/Poem/frontend/react/AggregationProfiles.js @@ -1,25 +1,20 @@ -import React, { useState, useMemo, useContext } from 'react'; +import React, { useState, useMemo, useContext, useEffect } from 'react'; import {Link} from 'react-router-dom'; import { LoadingAnim, BaseArgoView, NotifyOk, - DropDown, - Icon, DiffElement, - ProfileMainInfo, + ProfileMain, NotifyError, ErrorComponent, ParagraphTitle, ProfilesListTable, - CustomError, - CustomReactSelect + CustomReactSelect, + CustomError } from './UIElements'; -import Autosuggest from 'react-autosuggest'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Formik, Field, FieldArray, Form } from 'formik'; import { faPlus, faTimes, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; -import FormikEffect from './FormikEffect.js'; import {Backend, WebApi} from './DataManager'; import { Alert, @@ -29,6 +24,7 @@ import { CardFooter, CardHeader, Col, + Form, FormGroup, FormText, Label, @@ -36,7 +32,9 @@ import { ButtonDropdown, DropdownToggle, DropdownMenu, - DropdownItem + DropdownItem, + Input, + FormFeedback } from 'reactstrap'; import { useQuery, useMutation, useQueryClient } from 'react-query'; import * as Yup from 'yup'; @@ -51,9 +49,12 @@ import { fetchMetricProfiles, fetchUserDetails } from './QueryFunctions'; +import { Controller, FormProvider, useFieldArray, useForm, useFormContext, useWatch } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { ErrorMessage } from '@hookform/error-message'; -const AggregationProfilesChangeContext = React.createContext(); +const AggregationProfilesChangeContext = React.createContext() const AggregationProfilesSchema = Yup.object().shape({ @@ -77,80 +78,89 @@ const AggregationProfilesSchema = Yup.object().shape({ }) -function insertSelectPlaceholder(data, text) { - if (data) - return [text, ...data] - else - return [text] +const DropDown = ({ + name, + options, + isnew=false, + errors=undefined +}) => { + const { control, setValue, clearErrors } = useFormContext() + + return ( + + { + setValue(name, e.value) + clearErrors(name) + }} + options={ options.map(option => new Object({ label: option, value: option })) } + value={ field.value ? { label: field.value, value: field.value } : undefined } + error={ errors } + isnew={ isnew } + /> + } + /> + ) } -const AggregationProfileAutocompleteField = ({service, index, isNew, groupNew, groupIndex, isMissing}) => { +const AggregationProfileAutocompleteField = ({ + index, + isNew, + groupNew, + groupIndex, + isMissing, + error +}) => { const context = useContext(AggregationProfilesChangeContext); - const [suggestionList, setSuggestions] = useState(context.list_services) + + const { control, setValue, clearErrors } = useFormContext() + + const name = `groups.${groupIndex}.services.${index}.name` return ( - context.formikBag.form.setFieldValue(`groups.${groupIndex}.services.${index}.name`, newValue), - value: service.name - }} - getSuggestionValue={(suggestion) => suggestion} - suggestions={suggestionList} - renderSuggestion={(suggestion, {_, isHighlighted}) => -
- {suggestion ? : ''} {suggestion} -
} - onSuggestionsFetchRequested={({ value }) => - { - let result = context.list_services.filter(service => service.toLowerCase().includes(value.trim().toLowerCase())) - setSuggestions(result) - } + + { + setValue(name, e.value) + clearErrors(name) + }} + options={ context.list_services.map(option => new Object({ label: option, value: option })) } + value={ field.value ? { label: field.value, value: field.value } : undefined } + error={ error } + isnew={ isNew && !groupNew } + ismissing={ isMissing } + /> } - onSuggestionsClearRequested={() => { - setSuggestions([]) - }} - onSuggestionSelected={(_, {suggestion}) => { - context.formikBag.form.setFieldValue(`groups.${groupIndex}.services.${index}.name`, suggestion) - }} - shouldRenderSuggestions={() => true} - theme={{ - suggestionsContainerOpen: 'aggregation-autocomplete-menu', - suggestionsList: 'aggregation-autocomplete-list' - }} /> ) } -const GroupList = ({name}) => { - const context = useContext(AggregationProfilesChangeContext); +const GroupList = () => { + const { control } = useFormContext() + + const { fields, insert, remove } = useFieldArray({ control, name: "groups" }) return ( { - context.formikBag.form.values[name].map((group, i) => - ( - - )} + fields.map((group, i) => + ) } @@ -159,56 +169,69 @@ const GroupList = ({name}) => { } -const Group = ({operation, services, groupindex, isnew, last}) => { - const context = useContext(AggregationProfilesChangeContext); +const Group = ({ group, groupindex, remove, insert, last }) => { + const context = useContext(AggregationProfilesChangeContext) + + const { control, formState: { errors } } = useFormContext() if (!last) return ( - + - + - + + } + /> + + + { message } + + } /> - - ( - )} +
+
@@ -216,9 +239,9 @@ const Group = ({operation, services, groupindex, isnew, last}) => {
@@ -227,395 +250,650 @@ const Group = ({operation, services, groupindex, isnew, last}) => { else return ( - + ) } -const ServiceList = ({services, groupindex, groupnew=false}) => +const ServiceList = ({groupindex, groupnew=false}) => { const context = useContext(AggregationProfilesChangeContext); + const { control } = useFormContext() + + const { fields: services, insert, remove } = useFieldArray({ control, name: `groups.${groupindex}.services` }) + return ( services.map((service, i) => - ( - - )} + ) ) } -const Service = ({service, operation, groupindex, groupnew, index, isnew, - serviceInsert, serviceRemove, ismissing}) => { +const Service = ({ + groupindex, + groupnew, + isnew, + index, + serviceInsert, + serviceRemove, + ismissing +}) => { const context = useContext(AggregationProfilesChangeContext); + const { control, getValues, formState: { errors } } = useFormContext() + + const insertOperationFromPrevious = (_, array) => { + if (array.length) { + let last = array.length - 1 + + return array[last]['operation'] + } + else + return '' + } + return ( - + + } /> + -
+
+ - - { - context.formikBag.form.errors && context.formikBag.form.errors.groups && context.formikBag.form.errors.groups[groupindex] && - context.formikBag.form.errors.groups[groupindex].services && context.formikBag.form.errors.groups[groupindex].services[index] && - context.formikBag.form.errors.groups[groupindex].services[index].name && - - - - } - { - context.formikBag.form.errors && context.formikBag.form.errors.groups && context.formikBag.form.errors.groups[groupindex] && - context.formikBag.form.errors.groups[groupindex].services && context.formikBag.form.errors.groups[groupindex].services[index] && - context.formikBag.form.errors.groups[groupindex].services[index].operation && - - - - } + + + + { message } + + } + /> + + + + + { message } + + } + /> + ) } -const AggregationProfilesForm = ({ values, errors, setFieldValue, historyview=false, addview=false, write_perm=false, - list_user_groups, logic_operations, endpoint_groups, list_id_metric_profiles }) => -( - <> - { + const context = useContext(AggregationProfilesChangeContext) + + const [listServices, setListServices] = useState([]) + const [isServiceMissing, setIsServiceMissing] = useState(false) + const [extraServices, setExtraServices] = useState([]) + const [areYouSureModal, setAreYouSureModal] = useState(false) + const [modalMsg, setModalMsg] = useState(undefined); + const [modalTitle, setModalTitle] = useState(undefined); + const [onYes, setOnYes] = useState('') + const [dropdownOpen, setDropdownOpen] = useState(false); + const hiddenFileInput = React.useRef(null); + + const extractListOfServices = (profileFromAggregation, listMetricProfiles) => { + let targetProfile = listMetricProfiles.filter(profile => profile.name === profileFromAggregation) + + if (targetProfile.length) { + let services = targetProfile[0].services.map(service => service.service) + return services.sort(sortServices) + } + else + return [] + } + + const handleFileRead = (e) => { + let jsonData = JSON.parse(e.target.result); + methods.setValue('metric_operation', jsonData.metric_operation); + methods.setValue('profile_operation', jsonData.profile_operation); + methods.setValue('metric_profile', jsonData.metric_profile); + methods.setValue('endpoint_group', jsonData.endpoint_group) + let groups = insertDummyGroup( + insertEmptyServiceForNoServices(jsonData.groups) + ) + methods.resetField("groups") + methods.setValue('groups', groups) + } + + const handleFileChosen = (file) => { + var reader = new FileReader(); + reader.onload = handleFileRead; + reader.readAsText(file); + } + + const onYesCallback = () => { + if (onYes === 'delete') + doDelete(methods.getValues("id")); + else if (onYes === 'change') + doChange(methods.getValues()); + } + + const methods = useForm({ + defaultValues: initValues, + mode: "all", + resolver: yupResolver(AggregationProfilesSchema) + }) + + const { control } = methods + + const metric_profile = useWatch({ control, name: "metric_profile" }) + const groups = useWatch({ control, name: "groups" }) + + useEffect(() => { + if (!publicView && !historyview) + setListServices(extractListOfServices(metric_profile, context.metric_profiles)) + }, [metric_profile]) + + useEffect(() => { + setIsServiceMissing(checkIfServiceMissingInMetricProfile()) + setExtraServices(checkIfServiceExtraInMetricProfile(listServices, groups)) + }, [groups, listServices]) + + + const checkIfServiceMissingInMetricProfile = () => { + let servicesInMetricProfiles = new Set(listServices) + let isMissing = false + + groups.forEach(group => { + for (let service of group.services) { + if (!["dummy", ""].includes(service.name)) + if (!servicesInMetricProfiles.has(service.name)) { + isMissing = true + break + } } - profiletype='aggregation' - addview={addview} - /> - - - - - - - - { + }) + + return isMissing + } + + const checkIfServiceExtraInMetricProfile = () => { + let serviceGroupsInAggregationProfile = new Set() + let _difference = new Set(listServices) + + groups.forEach(group => { + for (let service of group.services) { + if (service.name !== "dummy") + serviceGroupsInAggregationProfile.add(service.name) + } + }) + + for (let elem of serviceGroupsInAggregationProfile) { + _difference.delete(elem) + } + + return Array.from(_difference).sort(sortServices) + } + + const onSubmitHandle = () => { + setAreYouSureModal(!areYouSureModal); + setModalMsg(`Are you sure you want to ${addview ? "add" : "change"} aggregation profile?`) + setModalTitle(`${addview ? "Add" : "Change"} aggregation profile`) + setOnYes('change') + } + + return ( + setAreYouSureModal(!areYouSureModal) } + addview={ publicView ? !publicView : addview } + publicview={ publicView } + submitperm={ !historyview && context.write_perm } + extra_button={ + (!addview && !historyview) && + setDropdownOpen(!dropdownOpen) }> + JSON + + { + let valueSave = JSON.parse(JSON.stringify(methods.getValues())); + removeDummyGroup(valueSave); + removeIsNewFlag(valueSave); + const jsonContent = { + endpoint_group: valueSave.endpoint_group, + metric_operation: valueSave.metric_operation, + profile_operation: valueSave.profile_operation, + metric_profile: valueSave.metric_profile, + groups: valueSave.groups + } + let filename = `${profile_name}.json` + downloadJSON(jsonContent, filename) + }} + > + Export + + {hiddenFileInput.current.click()}} + > + Import + + + { handleFileChosen(e.target.files[0]) }} + style={{display: 'none'}} + /> + + } + > + +
onSubmitHandle(val)) } data-testid="aggregation-form" > + { + (isServiceMissing && !(publicView || historyview)) && + +
+   + Some Service Flavours used in Aggregation profile are not presented in associated Metric profile meaning that two profiles are out of sync. Check below for Service Flavours in blue borders. +
+
+ } + { + (groups.length > 1 && extraServices.length > 0 && !(publicView || historyview || addview)) && + +
+

+   + There are some extra Service Flavours in associated metric profile which are not used in the aggregation profile, meaning that two profiles are out of sync: +

+

{ extraServices.join(', ') }

+
+
+ } + - - - + undefined + : + context.write_perm ? + context.list_user_groups + : + [methods.getValues("groupname")] + } + profiletype="aggregation" + addview={ addview } + /> + + + + + + { + historyview && + + + + } - + historyview ? + + : + { + methods.setValue("metric_operation", e.value) + methods.clearErrors("metric_operation") + }} + options={ + context.logic_operations.map(operation => new Object({ + label: operation, value: operation + })) + } + value={ field.value ? { label: field.value, value: field.value } : undefined } + error={ methods.formState.errors?.metric_operation } + label="Metric operation:" + /> + } /> + - - : - - setFieldValue('metric_operation', e.value) - } - options={ - logic_operations.map(operation => new Object({ - label: operation, value: operation - })) - } - value={ - values.metric_operation ? - { label: values.metric_operation, value: values.metric_operation } - : undefined - } - error={errors.metric_operation} - label='Metric operation:' - /> - - } - - - - - - Logical operation that will be applied between metrics of each service flavour - - - - - - - - - { - historyview ? - <> + + - + + Logical operation that will be applied between metrics of each service flavour + + + + + + + + { + historyview && + + + + } - + historyview ? + + : + { + methods.setValue("profile_operation", e.value) + methods.clearErrors("profile_operation") + }} + options={ + context.logic_operations.map(operation => new Object({ + label: operation, value: operation + })) + } + value={ field.value ? { label: field.value, value: field.value } : undefined } + label="Aggregation operation:" + error={ methods.formState.errors?.profile_operation } + /> + } /> - - : - - setFieldValue('profile_operation', e.value)} - options={ - logic_operations.map(operation => new Object({ - label: operation, value: operation - })) - } - value={ - values.profile_operation ? - { label: values.profile_operation, value: values.profile_operation } - : undefined - } - label='Aggregation operation:' - error={errors.profile_operation} - /> - - } - - - - - - Logical operation that will be applied between defined service flavour groups - - - - - - - - - { - historyview ? - <> + + - + + + Logical operation that will be applied between defined service flavour groups + + + + + + + + { + historyview && + + + + } - + historyview ? + + : + { + methods.setValue("endpoint_group", e.value) + methods.clearErrors("endpoint_group") + }} + options={ + context.endpoint_groups.map(group => new Object({ + label: group, value: group + })) + } + value={ field.value ? { label: field.value, value: field.value } : undefined } + label="Endpoint group:" + error={ methods.formState.errors?.endpoint_group } + /> + } /> - - : - - setFieldValue('endpoint_group', e.value) - } - options={ - endpoint_groups.map(group => new Object({ - label: group, value: group - })) - } - value={ - values.endpoint_group ? - { label: values.endpoint_group, value: values.endpoint_group } - : undefined - } - label='Endpoint group:' - error={errors.endpoint_group} - /> - - } - + + + + - - - - - - - { - historyview ? - <> - - + + + { + historyview && + } + + historyview ? + + : + { + methods.setValue("metric_profile", e.value) + methods.clearErrors("metric_profile") + }} + options={ + extractListOfMetricsProfiles(context.metric_profiles).map(profile => new Object({ + label: profile.name, value: profile.name + })) + } + value={ field.value ? { label: field.value, value: field.value } : undefined } + label="Metric profile:" + error={ methods.formState.errors?.metric_profile } + /> + } /> - + + + Metric profile associated to Aggregation profile. Service flavours defined in service flavour groups originate from selected metric profile. + + + + + + { + !(publicView || historyview) ? + + + : - setFieldValue('metric_profile', e.value)} - options={ - list_id_metric_profiles.map(profile => new Object({ - label: profile.name, value: profile.name - })) - } - value={ - values.metric_profile ? - { label: values.metric_profile, value: values.metric_profile } - : undefined + + } + { + (!historyview && context.write_perm) && +
+ { + !addview ? + + : +
} - label='Metric profile:' - error={errors.metric_profile} - /> + +
} - - - Metric profile associated to Aggregation profile. Service flavours defined in service flavour groups originate from selected metric profile. - - - - - - -); + +
+
+ ) +} -const GroupsDisabledForm = ( props ) => ( - ( - - { - props.values['groups'].map((group, i) => - ( - - - - - - - {props.values.groups[i].name} - - - - - { - group.services.map((_, j) => - ( - - - {props.values.groups[i].services[j].name} - - - {props.values.groups[i].services[j].operation} - - - )} - /> - ) - } - - - {props.values.groups[i].operation} - - - - -
- {props.values.profile_operation} -
- -
- )} - /> - ) - } -
- )} - /> -) +const GroupsDisabledForm = () => { + const { getValues } = useFormContext() + + return ( + + { + getValues("groups").map((group, i) => + + + + + + + { group.name } + + + + + { + group.services.map((_, j) => + + + { group.services[j].name } + + + { group.services[j].operation } + + + ) + } + + + { group.operation } + + + + +
+ { getValues("profile_operation") } +
+ +
+ ) + } +
+ ) +} const fetchAP = async (webapi, apiid) => { @@ -623,6 +901,73 @@ const fetchAP = async (webapi, apiid) => { } +const sortServices = (a, b) => { + if (a.toLowerCase() < b.toLowerCase()) return -1 + if (a.toLowerCase() > b.toLowerCase()) return 1 +} + + +const sortMetricProfiles = (a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + if (a.name.toLowerCase() === b.name.toLowerCase()) return 0; +} + + +const extractListOfMetricsProfiles = (allProfiles) => { + var list_profiles = [] + + allProfiles.forEach(profile => { + var i = list_profiles['length'] + var {name, id} = profile + + list_profiles[i] = {name, id} + i += 1 + }) + + return list_profiles.sort(sortMetricProfiles) +} + + +const insertDummyGroup = (groups) => { + return [...groups, {name: 'dummy', operation: 'OR', services: [{name: 'dummy', operation: 'OR'}]}] +} + + +const insertEmptyServiceForNoServices = (groups) => { + groups.forEach(group => { + if (group.services.length === 0) { + group.services.push({name: '', operation: ''}) + } + }) + return groups +} + + +const removeDummyGroup = (values) => { + let last_group_element = values.groups[values.groups.length - 1] + + if (last_group_element['name'] == 'dummy' && + last_group_element.services[0]['name'] == 'dummy') { + values.groups.pop() + } +} + + +const removeIsNewFlag = (values) => { + for (let group of values.groups) { + let keys = Object.keys(group) + if (keys.indexOf('isNew') !== -1) + delete group.isNew + for (let service of group.services) { + let keys = Object.keys(service) + if (keys.indexOf('isNew') !== -1) + delete service.isNew + } + } +} + + export const AggregationProfilesChange = (props) => { const tenant_name = props.tenantname; const profile_name = props.match.params.name; @@ -631,16 +976,6 @@ export const AggregationProfilesChange = (props) => { const location = props.location; const publicView = props.publicView; - const [listServices, setListServices] = useState(undefined); - const [areYouSureModal, setAreYouSureModal] = useState(false) - const [modalMsg, setModalMsg] = useState(undefined); - const [modalTitle, setModalTitle] = useState(undefined); - const [onYes, setOnYes] = useState('') - const [dropdownOpen, setDropdownOpen] = useState(false); - const [formikValues, setFormikValues] = useState({}) - const hiddenFileInput = React.useRef(null); - const formikRef = React.useRef(); - const backend = new Backend(); const webapi = new WebApi({ token: props.webapitoken, @@ -672,11 +1007,12 @@ export const AggregationProfilesChange = (props) => { { enabled: !addview && (!publicView ? !!userDetails : true), initialData: () => { - return queryClient.getQueryData( - [`${publicView ? 'public_' : ''}aggregationprofile`, 'backend'] - )?.find( - profile => profile.name === profile_name - ) + if (!addview) + return queryClient.getQueryData( + [`${publicView ? 'public_' : ''}aggregationprofile`, 'backend'] + )?.find( + profile => profile.name === profile_name + ) } } ) @@ -687,11 +1023,12 @@ export const AggregationProfilesChange = (props) => { { enabled: !!backendAP, initialData: () => { - return queryClient.getQueryData( - [`${publicView ? "public_" : ""}aggregationprofile`, "webapi"] - )?.find( - profile => profile.id == backendAP.apiid - ) + if (!addview) + return queryClient.getQueryData( + [`${publicView ? "public_" : ""}aggregationprofile`, "webapi"] + )?.find( + profile => profile.id == backendAP.apiid + ) } } ) @@ -711,25 +1048,6 @@ export const AggregationProfilesChange = (props) => { return '' } - const sortServices = (a, b) => { - if (a.toLowerCase() < b.toLowerCase()) return -1 - if (a.toLowerCase() > b.toLowerCase()) return 1 - } - - const extractListOfServices = (profileFromAggregation, listMetricProfiles) => { - let targetProfile = listMetricProfiles.filter(profile => profile.name === profileFromAggregation.name) - - if (targetProfile.length === 0) - targetProfile = listMetricProfiles.filter(profile => profile.id === profileFromAggregation.id) - - if (targetProfile.length) { - let services = targetProfile[0].services.map(service => service.service) - return services.sort(sortServices) - } - else - return [] - } - const sortMetricProfiles = (a, b) => { if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; @@ -750,44 +1068,6 @@ export const AggregationProfilesChange = (props) => { return list_profiles.sort(sortMetricProfiles) } - const insertEmptyServiceForNoServices = (groups) => { - groups.forEach(group => { - if (group.services.length === 0) { - group.services.push({name: '', operation: ''}) - } - }) - return groups - } - - const insertOperationFromPrevious = (_, array) => { - if (array.length) { - let last = array.length - 1 - - return array[last]['operation'] - } - else - return '' - } - - const onSubmitHandle = (values) => { - let msg = undefined; - let title = undefined; - - if (addview) { - msg = 'Are you sure you want to add Aggregation profile?' - title = 'Add aggregation profile' - } - else { - msg = 'Are you sure you want to change Aggregation profile?' - title = 'Change aggregation profile' - } - setAreYouSureModal(!areYouSureModal); - setModalMsg(msg) - setModalTitle(title) - setOnYes('change') - setFormikValues(values) - } - const doChange = (values) => { let valueSend = JSON.parse(JSON.stringify(values)); removeDummyGroup(valueSend) @@ -909,90 +1189,6 @@ export const AggregationProfilesChange = (props) => { }) } - const insertDummyGroup = (groups) => { - return [...groups, {name: 'dummy', operation: 'OR', services: [{name: 'dummy', operation: 'OR'}]}] - } - - const removeDummyGroup = (values) => { - let last_group_element = values.groups[values.groups.length - 1] - - if (last_group_element['name'] == 'dummy' && - last_group_element.services[0]['name'] == 'dummy') { - values.groups.pop() - } - } - - const removeIsNewFlag = (values) => { - for (let group of values.groups) { - let keys = Object.keys(group) - if (keys.indexOf('isNew') !== -1) - delete group.isNew - for (let service of group.services) { - let keys = Object.keys(service) - if (keys.indexOf('isNew') !== -1) - delete service.isNew - } - } - } - - const checkIfServiceMissingInMetricProfile = (servicesMetricProfile, serviceGroupsAggregationProfile) => { - let servicesInMetricProfiles = new Set(servicesMetricProfile) - let isMissing = false - - serviceGroupsAggregationProfile.forEach(group => { - for (let service of group.services) { - if (!servicesInMetricProfiles.has(service.name)) { - isMissing = true - break - } - } - }) - - return isMissing - } - - const checkIfServiceExtraInMetricProfile = (servicesMetricProfile, serviceGroupsAggregationProfile) => { - let serviceGroupsInAggregationProfile = new Set() - let _difference = new Set(servicesMetricProfile) - - serviceGroupsAggregationProfile.forEach(group => { - for (let service of group.services) { - serviceGroupsInAggregationProfile.add(service.name) - } - }) - - for (let elem of serviceGroupsInAggregationProfile) { - _difference.delete(elem) - } - - return Array.from(_difference).sort(sortServices) - } - - const handleFileRead = (e) => { - let jsonData = JSON.parse(e.target.result); - formikRef.current.setFieldValue('metric_operation', jsonData.metric_operation); - formikRef.current.setFieldValue('profile_operation', jsonData.profile_operation); - formikRef.current.setFieldValue('metric_profile', jsonData.metric_profile); - formikRef.current.setFieldValue('endpoint_group', jsonData.endpoint_group) - let groups = insertDummyGroup( - insertEmptyServiceForNoServices(jsonData.groups) - ) - formikRef.current.setFieldValue('groups', groups); - } - - const handleFileChosen = (file) => { - var reader = new FileReader(); - reader.onload = handleFileRead; - reader.readAsText(file); - } - - const onYesCallback = () => { - if (onYes === 'delete') - doDelete(formikValues.id); - else if (onYes === 'change') - doChange(formikValues); - } - if (loadingUserDetails || loadingBackendAP || loadingWebApiAP || loadingMetricProfiles) return () @@ -1006,10 +1202,6 @@ export const AggregationProfilesChange = (props) => { return () else if ((addview || (backendAP && webApiAP) && metricProfiles)) { - if (!listServices && !publicView && !addview) - setListServices(!addview ? extractListOfServices(webApiAP.metric_profile, metricProfiles) : []) - - let isServiceMissing = checkIfServiceMissingInMetricProfile(listServices, !addview ? webApiAP.groups : []) let write_perm = undefined if (publicView) { @@ -1025,57 +1217,15 @@ export const AggregationProfilesChange = (props) => { } return ( - setAreYouSureModal(!areYouSureModal)} - addview={publicView ? !publicView : addview} - publicview={publicView} - submitperm={write_perm} - extra_button={ - !addview && - setDropdownOpen(!dropdownOpen) }> - JSON - - { - let valueSave = JSON.parse(JSON.stringify(formikRef.current.values)); - removeDummyGroup(valueSave); - removeIsNewFlag(valueSave); - const jsonContent = { - endpoint_group: valueSave.endpoint_group, - metric_operation: valueSave.metric_operation, - profile_operation: valueSave.profile_operation, - metric_profile: valueSave.metric_profile, - groups: valueSave.groups - } - let filename = `${profile_name}.json` - downloadJSON(jsonContent, filename) - }} - > - Export - - {hiddenFileInput.current.click()}} - > - Import - - - { handleFileChosen(e.target.files[0]) }} - style={{display: 'none'}} - /> - - } - > - + { : webApiAP.groups }} - onSubmit={(values, actions) => onSubmitHandle(values, actions)} - validationSchema={AggregationProfilesSchema} - validateOnBlur={true} - validateOnChange={false} - innerRef={formikRef} - > - {props => { - let extraServices = checkIfServiceExtraInMetricProfile(listServices, props.values.groups) - return ( -
- { - if (current.values.metric_profile !== prev.values.metric_profile) { - let selected_profile = { - name: current.values.metric_profile - } - setListServices(extractListOfServices(selected_profile, - metricProfiles)) - } - }} - /> - { - (isServiceMissing && !publicView) && - -
-   - Some Service Flavours used in Aggregation profile are not presented in associated Metric profile meaning that two profiles are out of sync. Check below for Service Flavours in blue borders. -
-
- } - { - (extraServices.length > 0 && !publicView) && - -
-

-   - There are some extra Service Flavours in associated metric profile which are not used in the aggregation profile, meaning that two profiles are out of sync: -

-

{ extraServices.join(', ') }

-
-
- } - - { - !publicView ? - ( - - - - )} - /> - : - - } - { - (write_perm) && -
- { - !addview ? - - : -
- } - -
- } - - )}} -
-
+ resourcename={ publicView ? 'Aggregation profile details' : 'aggregation profile' } + profile_name={ profile_name } + location={ location } + publicView={ publicView } + historyview={ publicView } + addview={ addview } + doChange={ doChange } + doDelete={ doDelete } + /> + ) } else @@ -1418,33 +1478,20 @@ export const AggregationProfileVersionDetails = (props) => { }; return ( - - - {props => ( -
- - - - )} -
-
- ); + + ) } else - return null; + return null } diff --git a/poem/Poem/frontend/react/MetricOverrides.js b/poem/Poem/frontend/react/MetricOverrides.js index 7c0ece984..8b422da55 100644 --- a/poem/Poem/frontend/react/MetricOverrides.js +++ b/poem/Poem/frontend/react/MetricOverrides.js @@ -1,21 +1,124 @@ -import React, { useMemo, useState } from "react" +import React, { useEffect, useMemo, useState } from "react" import { useMutation, useQuery, useQueryClient } from "react-query" import { Link } from "react-router-dom" import { Backend } from "./DataManager" import { fetchUserDetails } from "./QueryFunctions" -import { BaseArgoTable, BaseArgoView, ErrorComponent, LoadingAnim, NotifyError, NotifyOk } from "./UIElements" -import { Formik, Form, Field, FieldArray } from "formik" +import { + BaseArgoTable, + BaseArgoView, + ErrorComponent, + LoadingAnim, + NotifyError, + NotifyOk +} from "./UIElements" import { FormGroup, InputGroup, InputGroupText, Row, Col, - Button + Button, + Form, + Input, + FormFeedback } from "reactstrap" import { ParagraphTitle } from "./UIElements" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faTimes, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { + Controller, + useFieldArray, + useForm, + useWatch +} from "react-hook-form" +import { yupResolver } from '@hookform/resolvers/yup'; +import * as Yup from "yup"; +import { ErrorMessage } from "@hookform/error-message" + + + +const validationSchema = Yup.object().shape({ + name: Yup.string() + .required("Name field is required") + .matches(/^[a-zA-Z][A-Za-z0-9\-_]*$/, "Name can contain alphanumeric characters, dash and underscore, but must always begin with a letter"), + globalAttributes: Yup.array().of( + Yup.object().shape({ + attribute: Yup.string().matches( + /^[a-zA-Z][A-Za-z0-9\-_.]*$/, { + excludeEmptyString: true, + message: "Attribute can contain alphanumeric characters, dash, underscore and dot, but must always begin with a letter" + } + ).when("value", { + is: (value) => !!value, + then: Yup.string().matches(/^[a-zA-Z][A-Za-z0-9\-_.]*$/, "Attribute can contain alphanumeric characters, dash, underscore and dot, but must always begin with a letter") + }), + value: Yup.string().when("attribute", { + is: (value) => !!value, + then: Yup.string().required("Attribute value is required") + }) + }, [[ "attribute", "value" ]]) + ), + hostAttributes: Yup.array().of( + Yup.object().shape({ + hostname: Yup.string() + .when(["attribute", "value"], { + is: (attribute, value) => !!value || !!attribute, + then: Yup.string().matches(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, "Invalid hostname"), + otherwise: Yup.string().matches( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, { + excludeEmptyString: true, + message: "Invalid hostname" + } + ) + }), + attribute: Yup.string().when(["hostname", "value"], { + is: (hostname, value) => !!hostname || !!value, + then: Yup.string().matches(/^[a-zA-Z][A-Za-z0-9\-_.]*$/, "Attribute can contain alphanumeric characters, dash, underscore and dot, but must always begin with a letter") + }), + value: Yup.string().when(["hostname", "attribute"], { + is: (hostname, attribute) => !!hostname || !!attribute, + then: Yup.string().required("Attribute value is required") + }) + }, [ + [ "hostname", "attribute" ], + [ "hostname", "value" ], + [ "attribute", "value" ] + ] ) + ), + metricParameters: Yup.array().of( + Yup.object().shape({ + hostname: Yup.string().when(["metric", "parameter", "value"], { + is: (metric, parameter, value) => !!metric || !!parameter || !!value, + then: Yup.string().matches(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, "Invalid hostname"), + otherwise: Yup.string().matches( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, { + excludeEmptyString: true, + message: "Invalid hostname" + } + ) + }), + metric: Yup.string().when(["hostname", "parameter", "value"], { + is: (hostname, parameter, value) => !!hostname || !!parameter || !!value, + then: Yup.string().matches(/^[a-zA-Z][A-Za-z0-9\-_.]*$/, "Metric name can contain alphanumeric characters, dash, underscore and dot, but must always begin with a letter") + }), + parameter: Yup.string().when(["hostname", "metric", "value"], { + is: (hostname, metric, value) => !!hostname || !!metric || !!value, + then: Yup.string().matches(/^\S+$/, "Parameter cannot contain white space") + }), + value: Yup.string().when(["hostname", "metric", "parameter"], { + is: (hostname, metric, parameter) => !!hostname || !!metric || !!parameter, + then: Yup.string().required("Parameter value is required") + }) + }, [ + [ "hostname", "metric" ], + [ "hostname", "parameter" ], + [ "hostname", "value" ], + [ "metric", "parameter" ], + [ "metric", "value" ], + [ "parameter", "value" ] + ]) + ) +}) const fetchOverrides = async () => { @@ -85,14 +188,15 @@ export const MetricOverrideList = (props) => { } -export const MetricOverrideChange = (props) => { - const name = props.match.params.name - const addview = props.addview - const location = props.location - const history = props.history - +const MetricOverrideForm = ({ + name=undefined, + override=undefined, + isSuperuser=false, + addview=false, + location=undefined, + history=undefined +}) => { const backend = new Backend() - const queryClient = useQueryClient() const addMutation = useMutation(async (values) => await backend.addObject("/api/v2/internal/metricconfiguration/", values)) @@ -103,33 +207,76 @@ export const MetricOverrideChange = (props) => { const [modalMsg, setModalMsg] = useState(undefined); const [modalTitle, setModalTitle] = useState(undefined); const [modalFlag, setModalFlag] = useState(undefined); - const [formValues, setFormValues] = useState(undefined); - const { data: userDetails, isLoading: loading } = useQuery( - 'userdetails', () => fetchUserDetails(true) - ); + const { control, handleSubmit, setValue, getValues, trigger, formState: { errors } } = useForm({ + defaultValues: { + id: override ? override.id : undefined, + name: override ? override.name : "", + globalAttributes: override ? override.global_attributes : [{ attribute: "", value: "" }], + hostAttributes: override ? override.host_attributes : [{ hostname: "", attribute: "", value: "" }], + metricParameters: override ? override.metric_parameters : [{ hostname: "", metric: "", parameter: "", value: "" }] + }, + mode: "all", + resolver: yupResolver(validationSchema) + }) - const { data: override, error: errorOverride, isLoading: loadingOverride } = useQuery( - ["metricoverride", name], async () => { return await backend.fetchData(`/api/v2/internal/metricconfiguration/${name}`)}, - { enabled: !addview && !!userDetails } - ) + const globalAttributes = useWatch({ control, name: "globalAttributes" }) + const hostAttributes = useWatch({ control, name: "hostAttributes" }) + const metricParameters = useWatch({ control, name: "metricParameters" }) + + const { fields: attrFields, insert: attrInsert, remove: attrRemove } = useFieldArray({ + control, + name: "globalAttributes" + }) + + const { fields: hostAttrFields, insert: hostAttrInsert, remove: hostAttrRemove } = useFieldArray({ + control, + name: "hostAttributes" + }) + + const { fields: mpFields, insert: mpInsert, remove: mpRemove } = useFieldArray({ + control, + name: "metricParameters" + }) + + useEffect(() => { + if (attrFields.length == 0) + setValue("globalAttributes", [{ attribute: "", value: "" }]) + + trigger("globalAttributes") + }, [globalAttributes]) + + useEffect(() => { + if (hostAttrFields.length == 0) + setValue("hostAttributes", [{ hostname: "", attribute: "", value: "" }]) + + trigger("hostAttributes") + }, [hostAttributes]) + + useEffect(() => { + if (mpFields.length == 0) + setValue("metricParameters", [{ hostname: "", metric: "", parameter: "", value: "" }]) + + trigger("metricParameters") + }, [metricParameters]) const toggleAreYouSure = () => { setAreYouSureModal(!areYouSureModal) } - const onSubmitHandle = (values) => { + const onSubmitHandle = () => { let msg = `Are you sure you want to ${addview ? "add" : "change"} metric configuration override?` let title = `${addview ? "Add" : "Change"} metric configuration override` setModalMsg(msg) setModalTitle(title) - setFormValues(values) setModalFlag("submit") toggleAreYouSure() } const doChange = () => { + let formValues = getValues() + let sendValues = { name: formValues.name, global_attributes: formValues.globalAttributes, @@ -196,338 +343,468 @@ export const MetricOverrideChange = (props) => { }) } - if (loading || loadingOverride) - return () - - else if (errorOverride) - return () - - else if ((!loading && addview) || override) - return ( - - onSubmitHandle(values)} - > - { props => ( -
- - - - - Name - - - - - - - - ( -
- - - - - - - - - - - { - props.values.globalAttributes.map((_, index) => - - - - - - - ) - } - -
#AttributeValueAction
{index + 1} - - - - - - -
-
- )} + undefined + }} + toggle={ toggleAreYouSure } + > + + + + + + Name + + + } /> - - - - ( -
- - - - - - - - - - - - { - props.values.hostAttributes.map((_, index) => - - - - - - - - ) - } - -
#HostnameAttributeValueAction
{index + 1} - - - - - - - - -
-
- )} - /> -
- - - ( -
- - - - - - - - - - - - - { props.values.metricParameters.map((_, index) => - - - - - - - - - ) } - -
#hostnamemetricparametervalueActions
{index + 1} - - - - - - - - - - -
-
- )} + + + { message } + + } /> -
- { - (userDetails.is_superuser) && -
- { - !addview ? + + + + + + +
+ + + + + + + + + + + { + attrFields.map((entry, index) => + + + + + + + ) + } + +
#AttributeValueAction
{ index + 1 } + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + - : -
- } - - +
+
+
+ + +
+ + + + + + + + + + + + { + hostAttrFields.map((entry, index) => + + + + + + + + ) + } + +
#HostnameAttributeValueAction
{ index + 1 } + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + + +
+
+
+ + +
+ + + + + + + + + + + + + { mpFields.map((entry, index) => + + + + + + + + + ) } + +
#hostnamemetricparametervalueActions
{ index + 1 } + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + + + } + /> + + + { message } + + } + /> + + + +
+
+
+ { + (isSuperuser) && +
+ { + !addview ? + + : +
} - - ) } - - + +
+ } + + + ) +} + + +export const MetricOverrideChange = (props) => { + const name = props.match.params.name + const addview = props.addview + const location = props.location + const history = props.history + + const backend = new Backend() + + const { data: userDetails, isLoading: loading } = useQuery( + 'userdetails', () => fetchUserDetails(true) + ); + + const { data: override, error: errorOverride, isLoading: loadingOverride } = useQuery( + ["metricoverride", name], async () => { return await backend.fetchData(`/api/v2/internal/metricconfiguration/${name}`)}, + { enabled: !addview && !!userDetails } + ) + + if (loading || loadingOverride) + return () + + else if (errorOverride) + return () + + else if (userDetails && ((!loading && addview) || override)) + return ( + ) else return null diff --git a/poem/Poem/frontend/react/MetricProfiles.js b/poem/Poem/frontend/react/MetricProfiles.js index a2746ebf5..fe9ff833c 100644 --- a/poem/Poem/frontend/react/MetricProfiles.js +++ b/poem/Poem/frontend/react/MetricProfiles.js @@ -1,7 +1,6 @@ -import React, { useState, useMemo, useContext } from 'react'; +import React, { useState, useMemo, useContext, useEffect } from 'react'; import {Link} from 'react-router-dom'; import {Backend, WebApi} from './DataManager'; -import Autosuggest from 'react-autosuggest'; import { LoadingAnim, BaseArgoView, @@ -9,20 +8,21 @@ import { NotifyOk, Icon, DiffElement, - ProfileMainInfo, NotifyError, ErrorComponent, ParagraphTitle, ProfilesListTable, - CustomError + CustomError, + ProfileMain, + CustomReactSelect } from './UIElements'; -import { Formik, Field, FieldArray, Form } from 'formik'; import { Button, ButtonDropdown, DropdownToggle, DropdownMenu, - DropdownItem + DropdownItem, + Form } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlus, faTimes, faSearch } from '@fortawesome/free-solid-svg-icons'; @@ -37,149 +37,154 @@ import { } from './QueryFunctions'; import './MetricProfiles.css'; +import { Controller, FormProvider, useFieldArray, useForm, useFormContext, useWatch } from 'react-hook-form'; +import * as yup from "yup" +import { yupResolver } from '@hookform/resolvers/yup'; export const MetricProfilesClone = (props) => ; export const MetricProfilesChange = (props) => ; -function matchItem(item, value) { - return item.toLowerCase().indexOf(value.toLowerCase()) !== -1; -} +const MetricProfilesComponentContext = React.createContext(); -const MetricProfilesComponentContext = React.createContext(); +const MetricProfilesSchema = yup.object().shape({ + name: yup.string().required("Required"), + groupname: yup.string().required("Required"), + view_services: yup.array() + .of(yup.object().shape({ + service: yup.string() + .required("Required") + .test("predefined_services", "Must be one of predefined service types", function (value) { + let arr = this.options.context.allServices.map(service => service.name) + if (arr.indexOf(value) === -1) + return false + + else + return true + }), + metric: yup.string() + .required("Required") + .test("predefined_metrics", "Must be one of predefined metrics", function (value) { + if (this.options.context.allMetrics.indexOf(value) == -1) + return false + + else + return true + }) + })) +}) -const MetricProfileAutocompleteField = ({suggestions, service, index, icon, tupleType, id}) => { - const context = useContext(MetricProfilesComponentContext); - const [suggestionList, setSuggestions] = useState(suggestions); +const MetricProfileAutocompleteField = ({ + tupleType, + index, + error, + isNew +}) => { + const context = useContext(MetricProfilesComponentContext) + + const { control, getValues, setValue, clearErrors } = useFormContext() + + const name = `view_services.${index}.${tupleType}` + + const options = tupleType === "service" ? + context.serviceflavours_all.map(service => service.name) + : + tupleType === "metric" ? + context.metrics_all + : + undefined const changeFieldValue = (newValue) => { - context.formikBag.form.setFieldValue(`view_services.${index}.${tupleType}`, newValue) - context.formikBag.form.setFieldValue(`view_services.${index}.${tupleType}Changed`, true) - context.onselect_handler(context.formikBag.form.values.view_services[index], - tupleType, - newValue) + const origIndex = context.listServices.findIndex(e => e.service === getValues(`view_services.${index}.service`) && e.metric === getValues(`view_services.${index}.metric`) && e.index === getValues(`view_services.${index}.index`)) + + if (getValues("view_services").length === 1 && getValues(name) == "") { + setValue(`view_services.${index}.isNew`, true) + setValue(`services.${origIndex}.isNew`, true) + } + + else { + setValue(`${name}Changed`, true) + setValue(`services.${origIndex}.${tupleType}Changed`, true) + } + + clearErrors("view_services") + setValue(`services.${origIndex}.${tupleType}`, newValue) + setValue(name, newValue) + clearErrors(name) } return ( - changeFieldValue(newValue), - value: service[tupleType] - }} - getSuggestionValue={(suggestion) => suggestion} - suggestions={suggestionList} - renderSuggestion={(suggestion, {query, isHighlighted}) => -
- {suggestion ? : ''} {suggestion} -
} - onSuggestionsFetchRequested={({ value }) => - { - let result = suggestions.filter(service => service.toLowerCase().includes(value.trim().toLowerCase())) - setSuggestions(result) - } + + changeFieldValue(e.value) } + options={ options.map(option => new Object({ label: option, value: option })) } + value={ field.value ? { label: field.value, value: field.value } : undefined } + error={ error || (!isNew && getValues("view_services")?.[index]?.[`${tupleType}Changed`]) } + isnew={ isNew } + /> } - onSuggestionsClearRequested={() => { - setSuggestions([]) - }} - onSuggestionSelected={(_, {suggestion}) => changeFieldValue(suggestion) } - shouldRenderSuggestions={() => true} - theme={{ - containerOpen: 'metricprofiles-autocomplete-menu', - suggestionsList: 'metricprofiles-autocomplete-list' - }} - id={id} /> ) } -const MetricProfileTupleValidate = ({view_services, name, groupname, - metrics_all, services_all}) => { - let errors = new Object() - let found = false - let empty = false - errors.view_services = new Array(view_services.length) - - // find duplicates - for (var i of view_services) - for (var j of view_services) - if (i.index !== j.index && - i.service === j.service && - i.metric === j.metric && - (i.isNew || i.serviceChanged - || i.metricChanged)) { - errors.view_services[i.index] = new Object() - errors.view_services[i.index].dup = "Duplicated" - found = true - } - - // empty essential metadata - if (!name) { - errors.name = 'Required' - empty = true +const sortServices = (a, b) => { + if (a.service.toLowerCase() < b.service.toLowerCase()) return -1; + if (a.service.toLowerCase() > b.service.toLowerCase()) return 1; + if (a.service.toLowerCase() === b.service.toLowerCase()) { + if (a.metric.toLowerCase() < b.metric.toLowerCase()) return -1; + if (a.metric.toLowerCase() > b.metric.toLowerCase()) return 1; + if (a.metric.toLowerCase() === b.metric.toLowerCase()) return 0; } - if (!groupname) { - errors.groupname = 'Required' - empty = true - } - - // find new empty tuples - for (let i of view_services) { - let obj = undefined - if (!errors.view_services[i.index]) - errors.view_services[i.index] = new Object() - obj = errors.view_services[i.index] - - if (!i.service && i.isNew) { - obj.service = "Required" - empty = true - } - else if (i.service && - (i.isNew || i.serviceChanged) && - services_all.map(service => service.name).indexOf(i.service) == -1) { - obj.service = "Must be one of predefined service types" - empty = true - } - if (!i.metric && i.isNew) { - obj.metric = "Required" - empty = true - } - else if (i.metric && - (i.isNew || i.metricChanged) && - metrics_all.indexOf(i.metric) == -1) { - obj.metric = "Must be one of predefined metrics" - empty = true - } - } - - if (found || empty) - return errors - else - return new Object() } const ServicesList = () => { const context = useContext(MetricProfilesComponentContext); + const { control, getValues, setValue, resetField, clearErrors, trigger, formState: { errors } } = useFormContext() + + const { fields, insert, remove } = useFieldArray({ control, name: "view_services" }) + + const onRemove = (index) => { + let tmpListServices = [ ...context.listServices ] + let origIndex = tmpListServices.findIndex(e => e.service == getValues(`view_services.${index}.service`) && e.metric == getValues(`view_services.${index}.metric`)) + tmpListServices.splice(origIndex, 1) + resetField("services") + setValue("services", tmpListServices) + remove(index) + clearErrors("view_services") + trigger("view_services") + } + + const onInsert = (index) => { + let tmpListServices = [ ...context.listServices ] + let origIndex = tmpListServices.findIndex(e => e.service === getValues(`view_services.${index}.service`) && e.metric === getValues(`view_services.${index}.metric`)) + let new_element = { service: "", metric: "", isNew: true } + tmpListServices.splice(origIndex, 0, new_element) + resetField("services") + setValue("services", tmpListServices) + insert(index + 1, new_element) + } + return ( - - - + + + { + !(context.publicView || context.historyview) && + + } @@ -188,117 +193,106 @@ const ServicesList = () => { - + { + !(context.publicView || context.historyview) && + + } { - context.formikBag.form.values.view_services.map((service, index) => - - - - - - + fields.map((service, index) => + !(context.publicView || context.historyview) ? + + + + + + + + { + errors?.view_services?.[index]?.dup && + + + + + + } + + : + + + + - { - context.formikBag.form.errors && context.formikBag.form.errors.view_services && context.formikBag.form.errors.view_services[index] - ? context.formikBag.form.errors.view_services[index].dup - ? - - - - - - : null - : null - } - ) } @@ -312,11 +306,271 @@ const fetchMetricProfile = async (webapi, apiid) => { } +const MetricProfilesForm = ({ + metricProfile, + userDetails, + metricsAll=undefined, + servicesAll=undefined, + doChange=undefined, + doDelete=undefined, + historyview=false, + ...props +}) => { + const profile_name = props.match.params.name; + const addview = props.addview + const location = props.location; + const cloneview = props.cloneview; + const publicView = props.publicView; + + const [areYouSureModal, setAreYouSureModal] = useState(false) + const [modalMsg, setModalMsg] = useState(undefined); + const [modalTitle, setModalTitle] = useState(undefined); + const [onYes, setOnYes] = useState('') + const [formikValues, setFormikValues] = useState({}) + const [dropdownOpen, setDropdownOpen] = useState(false); + const hiddenFileInput = React.useRef(null); + + const flattenServices = (services) => { + let flat_services = []; + + services.forEach((service_element) => { + let service = service_element.service; + service_element.metrics.forEach((metric) => { + flat_services.push({ service, metric }) + }) + }) + return flat_services + } + + let write_perm = undefined + + if (publicView) { + write_perm = false + } + else if (cloneview) { + write_perm = userDetails.is_superuser || + userDetails.groups.metricprofiles.length > 0; + } + else if (!addview) { + write_perm = userDetails.is_superuser || + userDetails.groups.metricprofiles.indexOf(metricProfile.groupname) >= 0; + } + else { + write_perm = userDetails.is_superuser || + userDetails.groups.metricprofiles.length > 0; + } + + const defaultServices = metricProfile.profile.services.length > 0 ? + historyview ? + metricProfile.profile.services.sort(sortServices) + : + flattenServices(metricProfile.profile.services).sort(sortServices) + : + [{ service: "", metric: "" }] + + const methods = useForm({ + defaultValues: { + id: metricProfile.profile.id, + name: metricProfile.profile.name, + description: metricProfile.profile.description, + groupname: metricProfile.groupname, + services: defaultServices, + view_services: defaultServices, + search_metric: "", + search_serviceflavour: "" + }, + mode: "all", + resolver: yupResolver(MetricProfilesSchema), + context: { allServices: servicesAll, allMetrics: metricsAll } + }) + + const { control } = methods + + const searchMetric = useWatch({ control, name: "search_metric" }) + const searchServiceFlavour = useWatch({ control, name: "search_serviceflavour" }) + const viewServices = useWatch({ control, name: "view_services" }) + const listServices = useWatch({ control, name: "services" }) + + useEffect(() => { + for (var i=0; i < viewServices.length; i++) + for (var j=0; j < viewServices.length; j++) + if (i !== j && viewServices[i].service === viewServices[j].service && viewServices[i].metric === viewServices[j].metric && (viewServices[i].isNew || viewServices[i].serviceChanged || viewServices[i].metricChanged)) { + methods.setError(`view_services.[${i}].dup`, { type: "custom", message: "Duplicated" }) + } + + if (viewServices.length === 0) { + methods.setValue("view_services", [{ service: "", metric: "" }]) + } + }, [viewServices]) + + useEffect(() => { + methods.setValue("view_services", listServices.filter(e => e.service.toLowerCase().includes(searchServiceFlavour.toLowerCase()) && e.metric.toLowerCase().includes(searchMetric.toLowerCase()))) + }, [searchMetric, searchServiceFlavour]) + + const onSubmitHandle = async (formValues) => { + let msg = `Are you sure you want to ${(addview || cloneview) ? "add" : "change"} metric profile?` + let title = `${(addview || cloneview) ? "Add" : "Change"} metric profile` + + setAreYouSureModal(!areYouSureModal); + setModalMsg(msg) + setModalTitle(title) + setOnYes('change') + setFormikValues(formValues) + } + + const onYesCallback = () => { + if (onYes === 'delete') + doDelete(formikValues.id); + else if (onYes === 'change') + doChange({ + formValues: formikValues, + servicesList: listServices.sort(sortServices) + } + ); + } + + return ( + setAreYouSureModal(!areYouSureModal)} + addview={publicView ? !publicView : addview} + publicview={publicView} + infoview={historyview} + submitperm={write_perm} + extra_button={ + !addview && + setDropdownOpen(!dropdownOpen)}> + CSV + + { + let csvContent = []; + listServices.sort(sortServices).forEach((service) => { + csvContent.push({service: service.service, metric: service.metric}) + }) + const content = PapaParse.unparse(csvContent); + let filename = `${profile_name}.csv`; + downloadCSV(content, filename) + }} + disabled={addview} + > + Export + + {hiddenFileInput.current.click()}} + > + Import + + + { + PapaParse.parse(e.target.files[0], { + header: true, + complete: (results) => { + var imported = results.data; + // remove entries without keys if there is any + imported = imported.filter( + obj => { + return 'service' in obj && 'metric' in obj + } + ) + imported.forEach(item => { + if (!listServices.some(service => { + return service.service === item.service && service.metric == item.metric + })) + item.isNew = true + }) + methods.resetField("view_services") + methods.setValue("view_services", imported.sort(sortServices)) + methods.resetField("search_metric") + methods.resetField("search_serviceflavour") + methods.resetField("services") + methods.setValue("services", imported.sort(sortServices)) + methods.trigger() + } + }) + }} + style={{display: 'none'}} + /> + + } + > + +
onSubmitHandle(val)) } data-testid="metricprofiles-form"> + + + + + + { + (!historyview && write_perm) && +
+ { + !addview && !cloneview ? + + : +
+ } + +
+ } + +
+
+ ) +} + + export const MetricProfilesComponent = (props) => { const profile_name = props.match.params.name; const addview = props.addview const history = props.history; - const location = props.location; const cloneview = props.cloneview; const publicView = props.publicView; @@ -327,6 +581,7 @@ export const MetricProfilesComponent = (props) => { serviceTypes: props.webapiservicetypes }) + const queryClient = useQueryClient(); const webapiChangeMutation = useMutation(async (values) => await webapi.changeMetricProfile(values)); const backendChangeMutation = useMutation(async (values) => await backend.changeObject('/api/v2/internal/metricprofiles/', values)); @@ -335,23 +590,6 @@ export const MetricProfilesComponent = (props) => { const webapiDeleteMutation = useMutation(async (idProfile) => await webapi.deleteMetricProfile(idProfile)); const backendDeleteMutation = useMutation(async (idProfile) => await backend.deleteObject(`/api/v2/internal/metricprofiles/${idProfile}`)); - - const [listServices, setListServices] = useState(undefined); - const [viewServices, setViewServices] = useState(undefined); - const [groupName, setGroupname] = useState(undefined); - const [metricProfileDescription, setMetricProfileDescription] = useState(undefined); - const [metricProfileName, setMetricProfileName] = useState(undefined); - const [areYouSureModal, setAreYouSureModal] = useState(false) - const [modalMsg, setModalMsg] = useState(undefined); - const [modalTitle, setModalTitle] = useState(undefined); - const [onYes, setOnYes] = useState('') - const [searchMetric, setSearchMetric] = useState(""); - const [searchServiceFlavour, setSearchServiceFlavour] = useState(""); - const [formikValues, setFormikValues] = useState({}) - const [dropdownOpen, setDropdownOpen] = useState(false); - const hiddenFileInput = React.useRef(null); - const formikRef = React.useRef(); - const { data: userDetails, error: errorUserDetails, isLoading: loadingUserDetails } = useQuery( 'userdetails', () => fetchUserDetails(true), { enabled: !publicView } @@ -394,112 +632,6 @@ export const MetricProfilesComponent = (props) => { { enabled: !!userDetails } ) - const onInsert = async (element, i, group, name, description) => { - // full list of services - if (searchServiceFlavour === '' && searchMetric === '') { - let service = element.service; - let metric = element.metric; - - let tmp_list_services = [...listServices]; - // split list into two preserving original - let slice_left_tmp_list_services = [...tmp_list_services].slice(0, i); - let slice_right_tmp_list_services = [...tmp_list_services].slice(i); - - slice_left_tmp_list_services.push({index: i, service, metric, isNew: true}); - - // reindex first slice - slice_left_tmp_list_services = ensureAlignedIndexes(slice_left_tmp_list_services) - - // reindex rest of list - let index_update = slice_left_tmp_list_services.length; - slice_right_tmp_list_services.forEach((element) => { - element.index = index_update; - index_update += 1; - }) - - // concatenate two slices - tmp_list_services = [...slice_left_tmp_list_services, ...slice_right_tmp_list_services]; - - setListServices(tmp_list_services); - setViewServices(tmp_list_services); - setGroupname(group); - setMetricProfileName(name); - setMetricProfileDescription(description); - } - // subset of matched elements of list of services - else { - let tmp_view_services = [...viewServices]; - let tmp_list_services = [...listServices]; - - let slice_left_view_services = [...tmp_view_services].slice(0, i) - let slice_right_view_services = [...tmp_view_services].slice(i) - - slice_left_view_services.push({...element, isNew: true}); - - let index_update = 0; - slice_left_view_services.forEach((element) => { - element.index = index_update; - index_update += 1; - }) - - index_update = i + 1; - slice_right_view_services.forEach((element) => { - element.index = index_update; - index_update += 1; - }) - - tmp_list_services.push({...element, isNew: true}) - - setViewServices([...slice_left_view_services, ...slice_right_view_services]); - setListServices(tmp_list_services); - } - } - - const handleSearch = (e, statefieldsearch, formikfield, alternatestatefield, - alternateformikfield) => { - let filtered = listServices; - let tmp_list_services = [...listServices]; - let searchWhat = statefieldsearch - - if (statefieldsearch === 'searchServiceFlavour') - statefieldsearch = searchServiceFlavour - else if (statefieldsearch === 'searchMetric') - statefieldsearch = searchMetric - - if (statefieldsearch.length > e.target.value.length) { - // handle remove of characters of search term - filtered = listServices.filter((elem) => matchItem(elem[formikfield], e.target.value)) - - tmp_list_services.sort(sortServices); - tmp_list_services = ensureAlignedIndexes(tmp_list_services) - } - else if (e.target.value !== '') { - filtered = listServices.filter((elem) => - matchItem(elem[formikfield], e.target.value)) - } - - if (alternatestatefield === 'searchServiceFlavour') - alternatestatefield = searchServiceFlavour - else if (alternatestatefield === 'searchMetric') - alternatestatefield = searchMetric - - // handle multi search - if (alternatestatefield.length) { - filtered = filtered.filter((elem) => - matchItem(elem[alternateformikfield], alternatestatefield)) - } - - filtered.sort(sortServices); - - if (searchWhat === 'searchServiceFlavour') - setSearchServiceFlavour(e.target.value); - else if (searchWhat === 'searchMetric') - setSearchMetric(e.target.value); - - setViewServices(filtered); - setListServices(tmp_list_services); - } - const doDelete = (idProfile) => { webapiDeleteMutation.mutate(idProfile, { onSuccess: () => { @@ -530,116 +662,6 @@ export const MetricProfilesComponent = (props) => { }) } - const onRemove = async (element, group, name, description) => { - let tmp_view_services = [] - let tmp_list_services = [] - let index = undefined - let index_tmp = undefined - - // special case when duplicates are result of explicit add of duplicated - // tuple followed by immediate delete of it - let dup_list = listServices.filter(service => - element.service === service.service && - element.metric === service.metric - ) - let dup_view = viewServices.filter(service => - element.service === service.service && - element.metric === service.metric - ) - let dup = dup_list.length >= 2 || dup_view.length >= 2 ? true : false - - if (dup) { - // search by index also - index = listServices.findIndex(service => - element.index === service.index && - element.service === service.service && - element.metric === service.metric - ); - index_tmp = viewServices.findIndex(service => - element.index === service.index && - element.service === service.service && - element.metric === service.metric - ); - } - else { - index = listServices.findIndex(service => - element.service === service.service && - element.metric === service.metric - ); - index_tmp = viewServices.findIndex(service => - element.service === service.service && - element.metric === service.metric - ); - } - - // don't remove last tuple, just reset it to empty values - if (viewServices.length === 1 - && listServices.length === 1) { - tmp_list_services = [{ - index: 0, - service: "", - metric: "" - }] - tmp_view_services = [{ - index: 0, - service: "", - metric: "" - }] - } - else if (index >= 0 && index_tmp >= 0) { - tmp_list_services = [...listServices] - tmp_view_services = [...viewServices] - tmp_list_services.splice(index, 1) - tmp_view_services.splice(index_tmp, 1) - - // reindex rest of list - for (var i = index; i < tmp_list_services.length; i++) { - let element_index = tmp_list_services[i].index - tmp_list_services[i].index = element_index - 1; - } - - for (let i = index_tmp; i < tmp_view_services.length; i++) { - let element_index = tmp_view_services[i].index - tmp_view_services[i].index = element_index - 1; - } - } - else { - tmp_list_services = [...listServices] - tmp_view_services = [...viewServices] - } - setListServices(ensureAlignedIndexes(tmp_list_services)); - setViewServices(ensureAlignedIndexes(tmp_view_services)); - setGroupname(group); - setMetricProfileName(name); - setMetricProfileDescription(description); - } - - const onSelect = (element, field, value) => { - let index = element.index; - let tmp_list_services = [...listServices]; - let tmp_view_services = [...viewServices]; - let new_element = tmp_list_services.findIndex(service => - service.index === index && service.isNew === true) - - if (new_element >= 0 ) { - tmp_list_services[new_element][field] = value; - tmp_list_services[new_element][field + 'Changed'] = value; - } - else { - tmp_list_services[index][field] = value; - tmp_list_services[index][field + 'Changed'] = value; - } - - for (var i = 0; i < tmp_view_services.length; i++) - if (tmp_view_services[i].index === index) { - tmp_view_services[i][field] = value - tmp_view_services[i][field + 'Changed'] = true - } - - setListServices(tmp_list_services); - setViewServices(tmp_view_services); - } - const groupMetricsByServices = (servicesFlat) => { let services = []; @@ -661,7 +683,7 @@ export const MetricProfilesComponent = (props) => { let services = []; let dataToSend = new Object() const backend_services = []; - formValues.view_services.forEach((service) => backend_services.push({ service: service.service, metric: service.metric })); + servicesList.forEach((service) => backend_services.push({ service: service.service, metric: service.metric })); if (!addview && !cloneview) { const { id } = webApiMP @@ -748,71 +770,6 @@ export const MetricProfilesComponent = (props) => { } } - const flattenServices = (services) => { - let flat_services = []; - let index = 0; - - services.forEach((service_element) => { - let service = service_element.service; - service_element.metrics.forEach((metric) => { - flat_services.push({index, service, metric}) - index += 1; - }) - }) - return flat_services - } - - const sortServices = (a, b) => { - if (a.service.toLowerCase() < b.service.toLowerCase()) return -1; - if (a.service.toLowerCase() > b.service.toLowerCase()) return 1; - if (a.service.toLowerCase() === b.service.toLowerCase()) { - if (a.metric.toLowerCase() < b.metric.toLowerCase()) return -1; - if (a.metric.toLowerCase() > b.metric.toLowerCase()) return 1; - if (a.metric.toLowerCase() === b.metric.toLowerCase()) return 0; - } - } - - const ensureAlignedIndexes = (list) => { - let i = 0 - - list.forEach(e => { - e.index = i - i += 1 - }) - - return list - } - - const onSubmitHandle = async (formValues) => { - let msg = undefined; - let title = undefined; - - if (addview || cloneview) { - msg = 'Are you sure you want to add Metric profile?' - title = 'Add metric profile' - } - else { - msg = 'Are you sure you want to change Metric profile?' - title = 'Change metric profile' - } - setAreYouSureModal(!areYouSureModal); - setModalMsg(msg) - setModalTitle(title) - setOnYes('change') - setFormikValues(formValues) - } - - const onYesCallback = () => { - if (onYes === 'delete') - doDelete(formikValues.id); - else if (onYes === 'change') - doChange({ - formValues: formikValues, - servicesList: listServices - } - ); - } - if (loadingUserDetails || loadingBackendMP || loadingWebApiMP || loadingMetricsAll || loadingWebApiST) return () @@ -833,10 +790,9 @@ export const MetricProfilesComponent = (props) => { else if ((addview && webApiST) || (backendMP && webApiMP && webApiST) || (publicView)) { - let write_perm = undefined - var metricProfile = { profile: { + id: "", name: '', description: '', services: [], @@ -846,266 +802,23 @@ export const MetricProfilesComponent = (props) => { } if (backendMP && webApiMP) { - metricProfile.profile = webApiMP; - metricProfile.groupname = backendMP.groupname; - if (publicView && !addview && !cloneview && !listServices && !viewServices) { - setMetricProfileName(metricProfile.profile.name); - setMetricProfileDescription(metricProfile.profile.description); - setGroupname(metricProfile.groupname); - setViewServices(flattenServices(metricProfile.profile.services).sort(sortServices)); - setListServices(flattenServices(metricProfile.profile.services).sort(sortServices)); - } - else if (!addview && !cloneview && !listServices && !viewServices) { - setMetricProfileName(metricProfile.profile.name); - setMetricProfileDescription(metricProfile.profile.description); - setGroupname(metricProfile.groupname); - setViewServices(ensureAlignedIndexes(flattenServices(metricProfile.profile.services).sort(sortServices))); - setListServices(ensureAlignedIndexes(flattenServices(metricProfile.profile.services).sort(sortServices))); - } - else if (cloneview && !viewServices && !listServices) { - setMetricProfileName('Cloned ' + metricProfile.profile.name); - metricProfile.profile.id = '' - setMetricProfileDescription(metricProfile.profile.description); - if (userDetails.groups.metricprofiles.indexOf(metricProfile.groupname) != -1) - setGroupname(metricProfile.groupname) - setViewServices(ensureAlignedIndexes(flattenServices(metricProfile.profile.services).sort(sortServices))); - setListServices(ensureAlignedIndexes(flattenServices(metricProfile.profile.services).sort(sortServices))); - } - } else { - if (addview && !cloneview && !viewServices && !listServices) { - setMetricProfileName(''); - setMetricProfileDescription(''); - setGroupname('') - setViewServices([{service: '', metric: '', index: 0, isNew: true}]); - setListServices([{service: '', metric: '', index: 0, isNew: true}]); - } - } + metricProfile.profile = webApiMP + metricProfile.groupname = backendMP.groupname - if (publicView) { - write_perm = false - } - else if (cloneview) { - write_perm = userDetails.is_superuser || - userDetails.groups.metricprofiles.length > 0; - } - else if (!addview) { - write_perm = userDetails.is_superuser || - userDetails.groups.metricprofiles.indexOf(metricProfile.groupname) >= 0; - } - else { - write_perm = userDetails.is_superuser || - userDetails.groups.metricprofiles.length > 0; + if (cloneview) + metricProfile.profile.name = `Cloned ${metricProfile.profile.name}` } return ( - setAreYouSureModal(!areYouSureModal)} - addview={publicView ? !publicView : addview} - publicview={publicView} - submitperm={write_perm} - extra_button={ - !addview && - setDropdownOpen(!dropdownOpen)}> - CSV - - { - let csvContent = []; - viewServices.forEach((service) => { - csvContent.push({service: service.service, metric: service.metric}) - }) - const content = PapaParse.unparse(csvContent); - let filename = `${profile_name}.csv`; - downloadCSV(content, filename) - }} - disabled={addview} - > - Export - - {hiddenFileInput.current.click()}} - > - Import - - - { - PapaParse.parse(e.target.files[0], { - header: true, - complete: (results) => { - var imported = results.data; - // remove entries without keys if there is any - imported = imported.filter( - obj => { - return 'service' in obj && 'metric' in obj - } - ) - imported.forEach(item => { - if (!viewServices.some(service => { - return service.service === item.service && service.metric == item.metric - })) - item.isNew = true - }) - setViewServices(ensureAlignedIndexes(imported).sort(sortServices)); - setListServices(ensureAlignedIndexes(imported).sort(sortServices)); - } - }) - }} - style={{display: 'none'}} - /> - - } - > - onSubmitHandle(values)} - enableReinitialize={true} - validate={MetricProfileTupleValidate} - innerRef={formikRef} - > - {props => ( -
- - - { - !publicView ? - ( - - - - )} - /> - : - ( -
# Service flavour MetricActions Service flavour MetricActions
- context.search_handler(e, 'searchServiceFlavour', - 'service', 'searchMetric', 'metric')} - component={SearchField} - /> + control={ control } + render={ ({ field }) => + + } + /> - context.search_handler(e, 'searchMetric', 'metric', - 'searchServiceFlavour', 'service')} - component={SearchField} - /> - - {''} + control={ control } + render={ ({ field }) => + + } + /> + {''} +
- {index + 1} - - service.name)} - service={service} - index={index} - icon='serviceflavour' - tupleType='service' - id={`autosuggest-metric-${index}`}/> - { - context.formikBag.form.errors && - context.formikBag.form.errors.view_services && - context.formikBag.form.errors.view_services[index] - ? context.formikBag.form.errors.view_services[index].service - ? - : null - : null - } - - - { - context.formikBag.form.errors && context.formikBag.form.errors.view_services && context.formikBag.form.errors.view_services[index] - ? context.formikBag.form.errors.view_services[index].metric - ? - : null - : null - } - - - -
+ {index + 1} + + + { + errors?.view_services?.[index]?.service && + + } + + + { + errors?.view_services?.[index]?.metric && + + } + + + +
+ +
{ index + 1 }{ service.service }{ service.metric }
- -
- - - - - - - - - - - - - - { - props.values.view_services.map((service, index) => - - - - - - ) - } - -
#Service flavourMetric
- - - handleSearch(e, - 'searchServiceFlavour', 'service', - 'searchMetric', 'metric')} - component={SearchField} - /> - - handleSearch(e, - 'searchMetric', 'metric', - 'searchServiceFlavour', 'service')} - component={SearchField} - /> -
{index + 1}{service.service}{service.metric}
- )} - /> - } - { - (write_perm) && -
- { - !addview && !cloneview ? - - : -
- } - -
- } - - )} - - + ) } @@ -1283,73 +996,45 @@ export const MetricProfileVersionDetails = (props) => { const name = props.match.params.name; const version = props.match.params.version; - const { data: metricProfileVersions, error, status } = useQuery( - ['metricprofile', 'versions', name], () => fetchMetricProfileVersions(name) + const { data: userDetails, error: errorUserDetails, isLoading: loadingUserDetails } = useQuery( + 'userdetails', () => fetchUserDetails(true) + ); + + const { data: metricProfileVersions, error, isLoading: loading } = useQuery( + ['metricprofile', 'versions', name], () => fetchMetricProfileVersions(name), + { enabled: !!userDetails } ) - if (status === 'loading') + if (loadingUserDetails || loading) return (); - else if (status === 'error') + else if (error) return (); + else if (errorUserDetails) + return () + else if (metricProfileVersions) { const instance = metricProfileVersions.find(ver => ver.version === version); - const metricProfileVersion = instance.fields; - metricProfileVersion.date_created = instance.date_created; + var metricProfile = { + profile: { + id: "", + name: instance.fields.name, + description: instance.fields.description, + services: instance.fields.metricinstances + }, + groupname: instance.fields.groupname, + date_created: instance.date_created + } return ( - - - {props => ( -
- - - ( - - - - - - - - - - { - props.values.metricinstances.map((service, index) => - - - - - - ) - } - -
#Service flavourMetric
{index + 1}{service.service}{service.metric}
- )} - /> - - )} -
-
+ ) } else - return null; + return null } diff --git a/poem/Poem/frontend/react/MetricTags.js b/poem/Poem/frontend/react/MetricTags.js index a5b3538f0..ef5e34303 100644 --- a/poem/Poem/frontend/react/MetricTags.js +++ b/poem/Poem/frontend/react/MetricTags.js @@ -17,7 +17,6 @@ import { SearchField } from "./UIElements" import { Backend } from "./DataManager" -import { Field, Formik } from "formik" import { Form, FormGroup, @@ -25,10 +24,22 @@ import { InputGroupText, Row, Col, - Button + Button, + Badge, + Input, + FormFeedback } from "reactstrap" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faSearch,faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { Controller, useForm, useWatch } from "react-hook-form" +import { ErrorMessage } from '@hookform/error-message' +import { yupResolver } from '@hookform/resolvers/yup' +import * as Yup from "yup" + + +const validationSchema = Yup.object().shape({ + name: Yup.string().required("This field is required") +}) export const MetricTagsList = (props) => { @@ -51,7 +62,26 @@ export const MetricTagsList = (props) => { accessor: "name", Cell: row => {row.value}, - column_width: "95%", + column_width: "20%", + Filter: DefaultColumnFilter + }, + { + Header: "Metrics", + accessor: "metrics", + Cell: row => +
+ { + row.value.length === 0 ? + none + : + row.value.map((metric, i) => + + { metric } + + ) + } +
, + column_width: "75%", Filter: DefaultColumnFilter } ], []) @@ -82,67 +112,59 @@ export const MetricTagsList = (props) => { } -export const MetricTagsComponent = (props) => { - const name = props.match.params.name - const publicView = props.publicView - const addview = props.addview - const location = props.location - const history = props.history +const MetricTagsForm = ({ + name=undefined, + tag=undefined, + allMetrics=undefined, + publicView=false, + addview=false, + location=undefined, + history=undefined +}) => { + + const backend = new Backend() + const queryClient = useQueryClient() - const [searchItem, setSearchItem] = useState(''); const [areYouSureModal, setAreYouSureModal] = useState(false); const [modalFlag, setModalFlag] = useState(undefined); const [modalTitle, setModalTitle] = useState(undefined); const [modalMsg, setModalMsg] = useState(undefined); - const [formValues, setFormValues] = useState(undefined); - - const backend = new Backend() - const queryClient = useQueryClient() const changeMutation = useMutation(async (values) => await backend.changeObject('/api/v2/internal/metrictags/', values)); const addMutation = useMutation(async (values) => await backend.addObject('/api/v2/internal/metrictags/', values)); const deleteMutation = useMutation(async () => await backend.deleteObject(`/api/v2/internal/metrictags/${name}`)) - const { data: userDetails, error: errorUserDetails, isLoading: loadingUserDetails } = useQuery( - "userdetails", () => fetchUserDetails(false), - { enabled: !publicView } - ) - - const { data: tag, error: errorTag, isLoading: loadingTag } = useQuery( - [`${publicView ? "public_" : ""}metrictags`, name], async () => { - return await backend.fetchData(`/api/v2/internal/${publicView ? "public_" : ""}metrictags/${name}`) - }, - { enabled: !addview } - ) - - const { data: metrics, error: errorMetrics, isLoading: loadingMetrics } = useQuery( - [`${publicView ? "public_" : ""}metrics4tags`, name], async () => { - return await backend.fetchData(`/api/v2/internal/${publicView ? "public_" : ""}metrics4tags/${name}`) + const { control, getValues, setValue, handleSubmit, formState: { errors } } = useForm({ + defaultValues: { + id: `${tag ? tag.id : ""}`, + name: `${tag ? tag.name : ""}`, + metrics4tag: tag?.metrics.length > 0 ? tag.metrics : [""], + searchItem: "" }, - { enabled: !addview } - ) + mode: "all", + resolver: yupResolver(validationSchema) + }) - const { data: allMetrics, error: errorAllMetrics, isLoading: loadingAllMetrics } = useQuery( - "metrictemplate", () => fetchMetricTemplates(publicView), - { enabled: !publicView } - ) + const searchItem = useWatch({ control, name: "searchItem" }) + const metrics4tag = useWatch({ control, name: "metrics4tag" }) const toggleAreYouSure = () => { setAreYouSureModal(!areYouSureModal) } - const onSubmitHandle = (values) => { + const onSubmitHandle = () => { let msg = `Are you sure you want to ${addview ? "add" : "change"} metric tag?` let title = `${addview ? "Add" : "Change "}metric tag` - setFormValues(values); - setModalMsg(msg); - setModalTitle(title); - setModalFlag('submit'); - toggleAreYouSure(); + setModalMsg(msg) + setModalTitle(title) + setModalFlag('submit') + toggleAreYouSure() } const doChange = () => { + let formValues = getValues() + const sendValues = new Object({ name: formValues.name, metrics: formValues.metrics4tag.filter(met => met !== "") @@ -179,9 +201,7 @@ export const MetricTagsComponent = (props) => { changeMutation.mutate({ ...sendValues, id: formValues.id }, { onSuccess: (response) => { queryClient.invalidateQueries("public_metrictags") - queryClient.invalidateQueries(["public_metrics4tags", name]) queryClient.invalidateQueries("metrictags") - queryClient.invalidateQueries(["metrics4tags", name]) queryClient.invalidateQueries("metric") queryClient.invalidateQueries("public_metric") queryClient.invalidateQueries("metrictemplate") @@ -209,9 +229,7 @@ export const MetricTagsComponent = (props) => { deleteMutation.mutate(undefined, { onSuccess: () => { queryClient.invalidateQueries("public_metrictags") - queryClient.invalidateQueries(["public_metrics4tags", name]) queryClient.invalidateQueries("metrictags") - queryClient.invalidateQueries(["metrics4tags", name]) queryClient.invalidateQueries("metric") queryClient.invalidateQueries("public_metric") queryClient.invalidateQueries("metrictemplate") @@ -229,10 +247,234 @@ export const MetricTagsComponent = (props) => { }) } }) - } - if (loadingUserDetails || loadingTag || loadingMetrics || loadingAllMetrics) + return ( + +
+ + + + + Name + + + } + /> + + + { message } + + } + /> + + + + + + + + + + + + { + !publicView && + } + + + + + + + + { + metrics4tag.filter( + filteredRow => filteredRow.toLowerCase().includes(searchItem.toLowerCase()) + ).map((item, index) => + + + + + { + !publicView && + + } + + + ) + } + +
#Metric templateActions
+ + + + + } + /> +
+ { index + 1 } + + { + publicView ? + item + : + + { + let tmpMetrics = getValues("metrics4tag") + tmpMetrics[index] = e.value + setValue("metrics4tag", tmpMetrics) + } } + options={ + allMetrics.map( + met => met.name + ).filter( + met => !metrics4tag.includes(met) + ).map( + option => new Object({ label: option, value: option }) + )} + value={ { label: item, value: item } } + /> + } + /> + } + + + +
+
+ { + !publicView && +
+ { + !addview ? + + : +
+ } + +
+ } +
+
+ ) +} + + +export const MetricTagsComponent = (props) => { + const name = props.match.params.name + const publicView = props.publicView + const addview = props.addview + const location = props.location + const history = props.history + + const backend = new Backend() + + const { data: userDetails, error: errorUserDetails, isLoading: loadingUserDetails } = useQuery( + "userdetails", () => fetchUserDetails(false), + { enabled: !publicView } + ) + + const { data: tag, error: errorTag, isLoading: loadingTag } = useQuery( + [`${publicView ? "public_" : ""}metrictags`, name], async () => { + return await backend.fetchData(`/api/v2/internal/${publicView ? "public_" : ""}metrictags/${name}`) + }, + { enabled: !addview } + ) + + const { data: allMetrics, error: errorAllMetrics, isLoading: loadingAllMetrics } = useQuery( + "metrictemplate", () => fetchMetricTemplates(publicView), + { enabled: !publicView } + ) + + if (loadingUserDetails || loadingTag || loadingAllMetrics) return () else if (errorUserDetails) @@ -241,195 +483,20 @@ export const MetricTagsComponent = (props) => { else if (errorTag) return () - else if (errorMetrics) - return () - else if (errorAllMetrics) return () - else if ((addview || (tag && metrics)) && (publicView || (allMetrics && userDetails))) { + else if ((addview || tag ) && (publicView || (allMetrics && userDetails))) { return ( - - - { - props => ( -
- - - - - Name - - - - - - - - - - - - - { - !publicView && - } - - - - - - - - { - props.values.metrics4tag.filter( - filteredRow => filteredRow.toLowerCase().includes(searchItem.toLowerCase()) - ).map((item, index) => - - - - - { - !publicView && - - } - - - ) - } - -
#Metric templateActions
- - - setSearchItem(e.target.value)} - component={SearchField} - /> -
- { index + 1 } - - { - publicView ? - item - : - { - let tmpMetrics = props.values.metrics4tag - tmpMetrics[index] = e.value - props.setFieldValue("metrics4tag", tmpMetrics) - } } - options={ - allMetrics.map( - met => met.name - ).filter( - met => !props.values.metrics4tag.includes(met) - ).map( - option => new Object({ label: option, value: option }) - )} - value={ { label: item, value: item } } - /> - } - - - -
-
- { - !publicView && -
- { - !addview ? - - : -
- } - -
- } -
- ) - } -
-
+ ) } else return null diff --git a/poem/Poem/frontend/react/MetricTemplates.js b/poem/Poem/frontend/react/MetricTemplates.js index 7b38e00d3..c1d1a73fe 100644 --- a/poem/Poem/frontend/react/MetricTemplates.js +++ b/poem/Poem/frontend/react/MetricTemplates.js @@ -3,35 +3,15 @@ import { MetricForm} from './Metrics'; import { Backend } from './DataManager'; import { LoadingAnim, - BaseArgoView, NotifyOk, NotifyError, NotifyWarn, ErrorComponent } from './UIElements'; -import { Formik, Form } from 'formik'; -import { Button } from 'reactstrap'; -import * as Yup from 'yup'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { fetchMetricTags, fetchMetricTemplates, fetchMetricTemplateTypes, fetchMetricTemplateVersion, fetchProbeVersion, fetchProbeVersions } from './QueryFunctions'; -const MetricTemplateSchema = Yup.object().shape({ - name: Yup.string() - .matches(/^\S*$/, 'Name cannot contain white spaces') - .required('Required'), - type: Yup.string(), - probeversion: Yup.string().when('type', { - is: (val) => val === 'Active', - then: Yup.string().required('Required') - }), - probeexecutable: Yup.string().when('type', { - is: (val) => val === 'Active', - then: Yup.string().required('Required') - }) -}); - - export const MetricTemplateComponent = (props) => { const probeview = props.probeview; @@ -41,7 +21,6 @@ export const MetricTemplateComponent = (props) => { else name = props.match.params.name; - const location = props.location; const addview = props.addview; const cloneview = props.cloneview; const publicView = props.publicView; @@ -55,13 +34,22 @@ export const MetricTemplateComponent = (props) => { const changeMutation = useMutation(async (values) => await backend.changeObject('/api/v2/internal/metrictemplates/', values)); const deleteMutation = useMutation(async () => await backend.deleteObject(`/api/v2/internal/metrictemplates/${name}`)); - const [popoverOpen, setPopoverOpen] = useState(false); - const [areYouSureModal, setAreYouSureModal] = useState(false); - const [modalFlag, setModalFlag] = useState(undefined); - const [modalTitle, setModalTitle] = useState(undefined); - const [modalMsg, setModalMsg] = useState(undefined); const [formValues, setFormValues] = useState(undefined); + const saveFormValues = (values) => { + setFormValues(values) + } + + const emptyConfig = [ + { 'key': 'maxCheckAttempts', 'value': '' }, + { 'key': 'timeout', 'value': '' }, + { 'key': 'path', 'value': '' }, + { 'key': 'interval', 'value': '' }, + { 'key': 'retryInterval', 'value': '' } + ]; + + const emptyEntry = [ { 'key': '', 'value': '' }]; + const { data: types, error: typesError, isLoading: typesLoading } = useQuery( `${publicView ? 'public_' : ''}metrictemplatestypes`, () => fetchMetricTemplateTypes(publicView), @@ -97,32 +85,6 @@ export const MetricTemplateComponent = (props) => { } ); - const emptyConfig = [ - { 'key': 'maxCheckAttempts', 'value': '' }, - { 'key': 'timeout', 'value': '' }, - { 'key': 'path', 'value': '' }, - { 'key': 'interval', 'value': '' }, - { 'key': 'retryInterval', 'value': '' } - ]; - - const emptyEntry = [ { 'key': '', 'value': '' }]; - - function togglePopOver() { - setPopoverOpen(!popoverOpen); - } - - function toggleAreYouSure() { - setAreYouSureModal(!areYouSureModal); - } - - function onSubmitHandle(values) { - setModalMsg(`Are you sure you want to ${addview || cloneview ? 'add' : 'change'} metric template?`) - setModalTitle(`${addview || cloneview ? 'Add' : 'Change'} metric template`) - setModalFlag('submit'); - setFormValues(values); - toggleAreYouSure(); - } - function doChange() { function onlyUnique(value, index, self) { return self.indexOf(value) == index; @@ -248,94 +210,37 @@ export const MetricTemplateComponent = (props) => { else if ((addview || metricTemplate) && (publicView || tenantview || (metricTemplates && types && tags)) && probeVersions) { return ( - 0 ? metricTemplate.attribute : emptyEntry, + dependency: metricTemplate && metricTemplate.dependency.length > 0 ? metricTemplate.dependency : emptyEntry, + parameter: metricTemplate && metricTemplate.parameter.length > 0 ? metricTemplate.parameter : emptyEntry, + flags: metricTemplate && metricTemplate.flags.length > 0 ? metricTemplate.flags : emptyEntry, + file_attributes: metricTemplate && metricTemplate.files.length > 0 ? metricTemplate.files : emptyEntry, + file_parameters: metricTemplate && metricTemplate.fileparameter.length > 0 ? metricTemplate.fileparameter : emptyEntry, + tags: metricTemplate ? metricTemplate.tags.map(item => new Object({ value: item, label: item })) : [], + probe: metricTemplate && metricTemplate.probeversion ? probeVersions.find(prv => prv.object_repr === metricTemplate.probeversion).fields : {'package': ''} }} - toggle={toggleAreYouSure} - > - 0 ? metricTemplate.attribute : emptyEntry, - dependency: metricTemplate && metricTemplate.dependency.length > 0 ? metricTemplate.dependency : emptyEntry, - parameter: metricTemplate && metricTemplate.parameter.length > 0 ? metricTemplate.parameter : emptyEntry, - flags: metricTemplate && metricTemplate.flags.length > 0 ? metricTemplate.flags : emptyEntry, - file_attributes: metricTemplate && metricTemplate.files.length > 0 ? metricTemplate.files : emptyEntry, - file_parameters: metricTemplate && metricTemplate.fileparameter.length > 0 ? metricTemplate.fileparameter : emptyEntry, - tags: metricTemplate ? metricTemplate.tags.map(item => new Object({ value: item, label: item })) : [], - probe: metricTemplate && metricTemplate.probeversion ? probeVersions.find(prv => prv.object_repr === metricTemplate.probeversion).fields : {'package': ''} - }} - onSubmit = {(values) => onSubmitHandle(values)} - validationSchema={MetricTemplateSchema} - enableReinitialize={true} - > - {props => ( -
- new Object({ value: tag.name, label: tag.name })) : []} - probeversions={probeVersions} - metrictemplatelist={metricTemplates ? metricTemplates.map(met => met.name) : []} - /> - { - (!tenantview && !publicView) && -
- { - (!addview && !cloneview) ? - - : -
- } - -
- } - - )} -
-
+ isTenantSchema={ tenantview } + saveFormValues={ (val) => saveFormValues(val) } + doChange={ doChange } + doDelete={ doDelete } + types={ types ? types : [] } + alltags={ tags ? tags.map(tag => new Object({ value: tag.name, label: tag.name })) : [] } + probeversions={ probeVersions } + metrictemplatelist={ metricTemplates ? metricTemplates.map(met => met.name) : [] } + /> ); } else return null @@ -381,41 +286,30 @@ export const MetricTemplateVersionDetails = (props) => { { package : '' }; return ( - - - {props => ( -
- - - )} -
-
- ); + + ) } else return null; }; diff --git a/poem/Poem/frontend/react/Metrics.js b/poem/Poem/frontend/react/Metrics.js index e0b926af1..989311d7b 100644 --- a/poem/Poem/frontend/react/Metrics.js +++ b/poem/Poem/frontend/react/Metrics.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Backend, WebApi } from './DataManager'; import { Link } from 'react-router-dom'; import { @@ -15,13 +15,12 @@ import { DefaultColumnFilter, SelectColumnFilter, BaseArgoTable, - CustomError, DropdownWithFormText, CustomDropdownIndicator, CustomReactSelect } from './UIElements'; -import { Formik, Form, Field, FieldArray } from 'formik'; import { + Form, FormGroup, Row, Col, @@ -34,7 +33,9 @@ import { InputGroup, InputGroupText, ButtonToolbar, - Badge + Badge, + Input, + FormFeedback } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faInfoCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -53,15 +54,34 @@ import { fetchMetrics, fetchMetricProfiles } from './QueryFunctions'; - - -function validateConfig(value) { - let error; - if (!value) { - error = 'Required'; - } - return error; -} +import * as Yup from 'yup'; +import { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { ErrorMessage } from '@hookform/error-message'; +import { yupResolver } from '@hookform/resolvers/yup'; + + +const metricValidationSchema = Yup.object().shape({ + name: Yup.string() + .matches(/^\S*$/, 'Name cannot contain white spaces') + .required('Required'), + type: Yup.string(), + probeversion: Yup.string().when('type', { + is: (val) => val === 'Active', + then: Yup.string().required('Required') + }), + probeexecutable: Yup.string().when('type', { + is: (val) => val === 'Active', + then: Yup.string().required('Required') + }), + config: Yup.array().when("type", { + is: (val) => val === "Active", + then: Yup.array().of( + Yup.object().shape({ + value: Yup.string().required("Required") + }) + ) + }) +}) const InlineDiffElement = ({title, item1, item2}) => { @@ -124,178 +144,122 @@ function arraysEqual(arr1, arr2) { } -const InlineFields = ({values, errors, field, addnew=false, readonly=false, addview=undefined}) => ( -
- - ( - (values[field] && values[field].length > 0) ? ( - values[field].map((item, index) => ( - - { - !(values.type === 'Passive' && field !== 'flags') && - - - {(index === 0) && } - - - {(index === 0) && } - - - } - - { - !(values.type === 'Passive' && field !== 'flags') && - - - - } - - { - values.type === 'Active' && field === 'config' ? - - : - !(values.type === 'Passive' && field !== 'flags') && - - } - { - errors.config && field === 'config' && - errors.config[index] && - +const InlineFields = ({ + fieldname="", + fields=undefined, + insert=undefined, + remove=undefined, + control=undefined, + readOnly=false, + isPassive=false, + isMetric=false, + addnew=false, + addview=false, + errors=undefined +}) => { + return ( + <> + + { + fields.map((entry, index) => + + + + { index === 0 && } + + + { index === 0 && } + + + + + + } - - - { - (addnew && field !== 'config' && (values[field][0]['key'] !== '' || values[field][0]['value'] !== '' || values[field].length > 1)) && - + /> + + + + } - - - { - (addnew && field !== 'config' && index === values[field].length - 1) && - - + /> + { + fieldname === "config" && + + + { message } + + } + /> + } + + + { + (fieldname !== 'config' && (entry.key !== '' || entry.value !== '' || fields.length > 1) && !readOnly) && - - - } - - )) - ) : ( - !(values.type === 'Passive' && field !== 'flags') && - - - - - - - - - - - - { - addnew && - - - - - - } - + } + + + ) - )} - /> -
-) + } + { + (addnew && !readOnly) && + + + + + + } + + ) +} export const ProbeVersionLink = ({probeversion, publicView=false}) => ( @@ -857,342 +821,632 @@ const styles = { export const MetricForm = ({ obj_label='', - popoverOpen=undefined, - togglePopOver=undefined, + resourcename="", + initValues=undefined, isHistory=false, isTenantSchema=false, - addview=false, probeversions=[], groups=[], metrictemplatelist=[], types=[], alltags=[], - publicView=false, + saveFormValues=undefined, + doChange=undefined, + doDelete=undefined, + writePerm=undefined, ...props }) => { + const addview = props.addview + const cloneview = props.cloneview + const tenantview = props.tenantview + const publicView = props.publicView + const probeview = props.probeview + const location = props.location + + const resourcename_beautify = resourcename === "metric" ? "metric" : "metric template" + let list_probes = []; probeversions.forEach(prv => list_probes.push(prv.object_repr)); + const [popoverOpen, setPopoverOpen] = useState(false); + const [areYouSureModal, setAreYouSureModal] = useState(false); + const [modalFlag, setModalFlag] = useState(undefined); + const [modalTitle, setModalTitle] = useState(undefined); + const [modalMsg, setModalMsg] = useState(undefined); + + const { control, setValue, getValues, handleSubmit, formState: { errors } } = useForm({ + defaultValues: { + id: initValues.id ? initValues.id : "", + name: initValues.name, + probeversion: initValues.probeversion, + type: initValues.type, + group: initValues.group ? initValues.group: "", + description: initValues.description, + probeexecutable: initValues.probeexecutable, + parent: initValues.parent, + config: initValues.config, + attributes: initValues.attributes, + dependency: initValues.dependency, + parameter: initValues.parameter, + flags: initValues.flags, + file_attributes: initValues.file_attributes, + file_parameters: initValues.file_parameters, + tags: initValues.tags, + probe: initValues.probe, + package: initValues.probe ? initValues.probe.package: "" + }, + mode: "all", + resolver: yupResolver(metricValidationSchema) + }) + + const probe = useWatch({ control, name: "probe" }) + const type = useWatch({ control, name: "type" }) + const watchAttributes = useWatch({ control, name: "attributes" }) + const watchDependency = useWatch({ control, name: "dependency" }) + const watchParameter = useWatch({ control, name: "parameter" }) + const watchFlags = useWatch({ control, name: "flags" }) + + useEffect(() => { + let pkg = "" + if (type === "Active") + pkg = probe?.package + + setValue("package", pkg) + }, [probe]) + + useEffect(() => { + if (resourcename !== "metric") { + if (type === "Passive") + setValue("probeversion", "") + + else + if (!getValues("probeversion") && "name" in probe) + setValue("probeversion", `${probe.name} (${probe.version})`) + } + }, [type]) + + useEffect(() => { + if (attributes.length === 0) + setValue("attributes", [{ key: "", value: "" }]) + }, [watchAttributes]) + + useEffect(() => { + if (dependency.length === 0) + setValue("dependency", [{ key: "", value: "" }]) + }, [watchDependency]) + + useEffect(() => { + if (parameter.length === 0) + setValue("parameter", [{ key: "", value: "" }]) + }, [watchParameter]) + + useEffect(() => { + if (flags.length === 0) + setValue("flags", [{ key: "", value: "" }]) + }, [watchFlags]) + + const { fields: config, insert: configInsert, remove: configRemove } = useFieldArray({ + control, + name: "config" + }) + + const { fields: attributes, insert: attributesInsert, remove: attributesRemove } = useFieldArray({ + control, + name: "attributes" + }) + + const { fields: dependency, insert: dependencyInsert, remove: dependencyRemove } = useFieldArray({ + control, + name: "dependency" + }) + + const { fields: parameter, insert: parameterInsert, remove: parameterRemove } = useFieldArray({ + control, + name: "parameter" + }) + + const { fields: flags, insert: flagsInsert, remove: flagsRemove } = useFieldArray({ + control, + name: "flags" + }) + + function togglePopOver() { + setPopoverOpen(!popoverOpen) + } + + function toggleAreYouSure() { + setAreYouSureModal(!areYouSureModal) + } + + function onSubmitHandle(values) { + setModalMsg(`Are you sure you want to ${addview || cloneview ? 'add' : 'change'} ${resourcename_beautify}?`) + setModalTitle(`${addview || cloneview ? 'Add' : 'Change'} ${resourcename_beautify}`) + setModalFlag('submit') + saveFormValues(values) + toggleAreYouSure() + } + return ( - <> - - - - - Name - - - - - Metric name. - - - - - Type + +
onSubmitHandle(val)) } data-testid="metric-form"> + + + + + Name + + + } + /> + + + { message } + + } + /> + + + Metric name. + + + + + Type + + (isTenantSchema || isHistory || publicView) ? + + : + { + setValue('type', e.value) + let flags = getValues("flags") + if (e.value === 'Passive') { + let ind = getValues("flags").length; + if (ind === 1 && flags[0].key === '') { + flags = [{ key: "PASSIVE", value: "1" }] + } else { + flags[ind] = { key: "PASSIVE", value: "1" } + } + setValue("flags", flags) + } else if (e.value === 'Active') { + if (!getValues("probe")) + setValue('probe', {'package': ''}) + + if (getValues("config").length !== 5) + setValue( + 'config', + [ + { key: 'maxCheckAttempts', value: '' }, + { key: 'timeout', value: '' }, + { key: 'path', value: '' }, + { key: 'interval', value: '' }, + { key: 'retryInterval', value: '' }, + ] + ) + + let ind = undefined; + flags.forEach((e, index) => { + if (e.key === 'PASSIVE') { + ind = index; + } + }); + if (flags.length === 1) + flags.splice(ind, 1, {'key': '', 'value': ''}) + else + flags.splice(ind, 1) + setValue("flags", flags) + } + }} + options={ types } + value={ field.value } + error={ errors?.type } + /> + } + /> + + + Metric is of given type. + + + + + + + Probe + + type === 'Passive' ? + + : + (isHistory || isTenantSchema || publicView) ? + + : + { + setValue('probeversion', e.value) + let probeversion = probeversions.find(prv => prv.object_repr === e.value); + if (probeversion) + setValue('probe', probeversion.fields) + else + setValue('probe', {'package': ''}) + }} + options={ list_probes } + value={ field.value } + /> + } + /> + + + { message } + + } + /> + { - (isTenantSchema || isHistory || publicView) ? - + Probe name and version  + { + !isHistory && + <> + + { + (obj_label === 'metrictemplate' && (!isHistory && !isTenantSchema && !publicView)) ? + + + + + setValue('tags', value) } + options={ alltags } + components={{ CustomDropdownIndicator }} + defaultValue={ field.value } + styles={ styles } + /> + } /> - : - { - props.setFieldValue('type', e.value) - if (e.value === 'Passive') { - let ind = props.values.flags.length; - if (ind === 1 && props.values.flags[0].key === '') { - props.setFieldValue('flags[0].key', 'PASSIVE'); - props.setFieldValue('flags[0].value', '1'); - } else { - props.setFieldValue(`flags[${ind}].key`, 'PASSIVE') - props.setFieldValue(`flags[${ind}].value`, '1') - } - } else if (e.value === 'Active') { - if (!props.values.probe) - props.setFieldValue('probe', {'package': ''}) - - if (props.values.config.length !== 5) - props.setFieldValue( - 'config', - [ - { key: 'maxCheckAttempts', value: '' }, - { key: 'timeout', value: '' }, - { key: 'path', value: '' }, - { key: 'interval', value: '' }, - { key: 'retryInterval', value: '' }, - ] + + + : + + + +
+ { + getValues("tags").length === 0 ? + none + : + (obj_label === 'metrictemplate' && !isHistory) ? + getValues("tags").map((tag, i) => + + { tag.value } + ) - - let ind = undefined; - props.values.flags.forEach((e, index) => { - if (e.key === 'PASSIVE') { - ind = index; - } - }); - if (props.values.flags.length === 1) - props.values.flags.splice(ind, 1, {'key': '', 'value': ''}) - else - props.values.flags.splice(ind, 1) - } - }} - options={ types } - value={ props.values.type } - error={ props.errors.type } - /> - } - - - Metric is of given type. - - - - - - - Probe - { - props.values.type === 'Passive' ? - + + { tag } + + ) + } +
+ +
+ } + + + + +