From 5148bb607f3a61978f5d67f894956302b4437076 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 11:37:57 +0000 Subject: [PATCH 01/22] fix(28664): fix the edit button --- hivemq-edge/src/frontend/src/locales/en/translation.json | 2 +- .../frontend/src/modules/TopicFilters/TopicFilterManager.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 882a2deed..7988ffb20 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1022,7 +1022,7 @@ "aria-label": "Delete Topic Filter" }, "add": { - "aria-label": "Add a new Topic Filter" + "aria-label": "Edit the topic Filters" } } }, diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.tsx index 53ca3e4df..82f9224ac 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { ColumnDef } from '@tanstack/react-table' import { Button, ButtonGroup, Card, CardBody, Text, useDisclosure } from '@chakra-ui/react' -import { LuPencil, LuPlus, LuTrash, LuView } from 'react-icons/lu' +import { LuClipboardEdit, LuPencil, LuTrash, LuView } from 'react-icons/lu' import { TopicFilter, TopicFilterList } from '@/api/__generated__' import { useTopicFilterManager } from '@/modules/TopicFilters/hooks/useTopicFilterManager.ts' @@ -100,7 +100,7 @@ const TopicFilterManager: FC = () => { }} trigger={({ onOpen: onOpenArrayDrawer }) => ( - From be72cc48f4b382e92fafe62d142121d259e99790 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:05:18 +0000 Subject: [PATCH 02/22] feat(28664): add data-uri validation --- .../frontend/src/locales/en/translation.json | 13 ++++- .../utils/topic-filter.schema.spec.ts | 35 ++++++++++++ .../TopicFilters/utils/topic-filter.schema.ts | 56 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts create mode 100644 hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 7988ffb20..3942e3221 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1034,7 +1034,18 @@ }, "error": { "loading": "We cannot load the topic filters. Please try again later.", - "noSchemaSampled": "No schema could be inferred from the traffic on this topic filter. Please try again later." + "noSchemaSampled": "No schema could be inferred from the traffic on this topic filter. Please try again later.", + "schema": { + "noDataUri": "Not a valid data-url encoded JSONSchema", + "noScheme": "No scheme defined in the URI", + "noSchemeData": "The scheme of the uri is not defined as 'data'", + "noJsonSchemaMimeType": "The media types doesn't include the mandatory `application/schema+json`", + "noBase64MediaType": "The media types doesn't include the mandatory `base64`", + "noBase64Data": "The data is not properly encoded as a `base64` string", + "noJSON": "The data is not properly encoded as a `JSON` object", + "ajvValidationFails": "Internal validation error", + "ajvNoProperties": "Not a valid JSONSchema: `properties` is missing" + } }, "toast": { "description_loading": "Processing the request", diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts new file mode 100644 index 000000000..cc1417157 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect } from 'vitest' +import { decodeDataUriJsonSchema } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' + +interface Suite { + data: string + error: string +} + +describe('decodeDataUriJsonSchema', () => { + const tests: Suite[] = [ + { data: '', error: 'Not a valid data-url encoded JSONSchema' }, + { data: '123', error: 'Not a valid data-url encoded JSONSchema' }, + { data: '123,456', error: 'No scheme defined in the URI' }, + { data: '123:test,456', error: "The scheme of the uri is not defined as 'data'" }, + { data: 'data:test,456', error: "The media types doesn't include the mandatory `application/schema+json`" }, + { data: 'data:application/json,456', error: "The media types doesn't include the mandatory `base64`" }, + { data: 'data:application/json;base64,456', error: 'The data is not properly encoded as a `base64` string' }, + { + data: 'data:application/json;base64,ewogICJ0ZXN0IjogMQp9Cg==', + error: 'Not a valid JSONSchema: `properties` is missing', + }, + ] + + it.each(tests)('$data should throw $error', ({ data, error }) => { + expect(() => decodeDataUriJsonSchema(data)).toThrowError(error) + }) + + it('should return a valid json object', () => { + expect( + decodeDataUriJsonSchema( + 'data:application/json;base64,IHsKICAgICJ0eXBlIiA6ICJvYmplY3QiLAogICAgInByb3BlcnRpZXMiIDogewogICAgICAiZGVmaW5pdGlvbiIgOiB7CiAgICAgICAgInR5cGUiIDogIm9iamVjdCIsCiAgICAgICAgInByb3BlcnRpZXMiIDogewogICAgICAgICAgImRhdGFUeXBlIiA6IHsKICAgICAgICAgICAgInR5cGUiIDogInN0cmluZyIsCiAgICAgICAgICAgICJlbnVtIiA6IFsgIk5VTEwiLCAiQk9PTCIsICJCWVRFIiwgIldPUkQiLCAiRFdPUkQiLCAiTFdPUkQiLCAiVVNJTlQiLCAiVUlOVCIsICJVRElOVCIsICJVTElOVCIsICJTSU5UIiwgIklOVCIsICJESU5UIiwgIkxJTlQiLCAiUkVBTCIsICJMUkVBTCIsICJDSEFSIiwgIldDSEFSIiwgIlNUUklORyIsICJXU1RSSU5HIiwgIlRJTUUiLCAiTFRJTUUiLCAiREFURSIsICJMREFURSIsICJUSU1FX09GX0RBWSIsICJMVElNRV9PRl9EQVkiLCAiREFURV9BTkRfVElNRSIsICJMREFURV9BTkRfVElNRSIsICJSQVdfQllURV9BUlJBWSIgXSwKICAgICAgICAgICAgInRpdGxlIiA6ICJEYXRhIFR5cGUiLAogICAgICAgICAgICAiZGVzY3JpcHRpb24iIDogIlRoZSBleHBlY3RlZCBkYXRhIHR5cGUgb2YgdGhlIHRhZyIsCiAgICAgICAgICAgICJlbnVtTmFtZXMiIDogWyAiTnVsbCIsICJCb29sZWFuIiwgIkJ5dGUiLCAiV29yZCAodW5pdCAxNikiLCAiRFdvcmQgKHVpbnQgMzIpIiwgIkxXb3JkICh1aW50IDY0KSIsICJVU2ludCAodWludCA4KSIsICJVaW50ICh1aW50IDE2KSIsICJVRGludCAodWludCAzMikiLCAiVUxpbnQgKHVpbnQgNjQpIiwgIlNpbnQgKGludCA4KSIsICJJbnQgKGludCAxNikiLCAiRGludCAoaW50IDMyKSIsICJMaW50IChpbnQgNjQpIiwgIlJlYWwgKGZsb2F0IDMyKSIsICJMUmVhbCAoZG91YmxlIDY0KSIsICJDaGFyICgxIGJ5dGUgY2hhcikiLCAiV0NoYXIgKDIgYnl0ZSBjaGFyKSIsICJTdHJpbmciLCAiV1N0cmluZyIsICJUaW1pbmcgKER1cmF0aW9uIG1zKSIsICJMb25nIFRpbWluZyAoRHVyYXRpb24gbnMpIiwgIkRhdGUgKERhdGVTdGFtcCkiLCAiTG9uZyBEYXRlIChEYXRlU3RhbXApIiwgIlRpbWUgT2YgRGF5IChUaW1lU3RhbXApIiwgIkxvbmcgVGltZSBPZiBEYXkgKFRpbWVTdGFtcCkiLCAiRGF0ZSBUaW1lIChEYXRlVGltZVN0YW1wKSIsICJMb25nIERhdGUgVGltZSAoRGF0ZVRpbWVTdGFtcCkiLCAiUmF3IEJ5dGUgQXJyYXkiIF0KICAgICAgICAgIH0sCiAgICAgICAgICAidGFnQWRkcmVzcyIgOiB7CiAgICAgICAgICAgICJ0eXBlIiA6ICJzdHJpbmciLAogICAgICAgICAgICAidGl0bGUiIDogIlRhZyBBZGRyZXNzIiwKICAgICAgICAgICAgImRlc2NyaXB0aW9uIiA6ICJUaGUgd2VsbCBmb3JtZWQgYWRkcmVzcyBvZiB0aGUgdGFnIHRvIHJlYWQiCiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICAicmVxdWlyZWQiIDogWyAiZGF0YVR5cGUiLCAidGFnQWRkcmVzcyIgXSwKICAgICAgICAidGl0bGUiIDogImRlZmluaXRpb24iLAogICAgICAgICJkZXNjcmlwdGlvbiIgOiAiVGhlIGFjdHVhbCBkZWZpbml0aW9uIG9mIHRoZSB0YWcgb24gdGhlIGRldmljZSIKICAgICAgfSwKICAgICAgImRlc2NyaXB0aW9uIiA6IHsKICAgICAgICAidHlwZSIgOiAic3RyaW5nIiwKICAgICAgICAidGl0bGUiIDogImRlc2NyaXB0aW9uIiwKICAgICAgICAiZGVzY3JpcHRpb24iIDogIkEgaHVtYW4gcmVhZGFibGUgZGVzY3JpcHRpb24gb2YgdGhlIHRhZyIKICAgICAgfSwKICAgICAgIm5hbWUiIDogewogICAgICAgICJ0eXBlIiA6ICJzdHJpbmciLAogICAgICAgICJ0aXRsZSIgOiAibmFtZSIsCiAgICAgICAgImRlc2NyaXB0aW9uIiA6ICJuYW1lIG9mIHRoZSB0YWcgdG8gYmUgdXNlZCBpbiBtYXBwaW5ncyIKICAgICAgfQogICAgfSwKICAgICJyZXF1aXJlZCIgOiBbICJkZWZpbml0aW9uIiwgImRlc2NyaXB0aW9uIiwgIm5hbWUiIF0KICB9Cg==' + ) + ).toBeFalsy + }) +}) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts new file mode 100644 index 000000000..a62c7f195 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts @@ -0,0 +1,56 @@ +// data:content/type;base64, +import { RJSFSchema } from '@rjsf/utils' +import { Accept } from 'react-dropzone' +import validator from '@rjsf/validator-ajv8' + +import i18n from '@/config/i18n.config.ts' + +export const MIMETYPE_JSON = 'application/json' +export const MIMETYPE_JSON_SCHEMA = 'application/schema+json' +export const ACCEPT_JSON_SCHEMA: Accept = { + [MIMETYPE_JSON_SCHEMA]: ['.json'], +} +const DECODE_HEADER_SEPARATOR = ',' +const DECODE_SCHEME_SEPARATOR = ':' +const DECODE_MEDIA_TYPES_SEPARATOR = ';' +const DECODE_DATA = 'data' +const DECODE_BASE64 = 'base64' + +export interface UriInfo { + mimeType: string + options?: string[] + body: RJSFSchema +} + +export const decodeDataUriJsonSchema = (dataUrl: string) => { + const [header, data] = dataUrl.split(DECODE_HEADER_SEPARATOR) + if (!data || !header) throw new Error(i18n.t('topicFilter.error.schema.noDataUri')) + + const [scheme, mediaTypes] = header.split(DECODE_SCHEME_SEPARATOR) + if (!mediaTypes) throw new Error(i18n.t('topicFilter.error.schema.noScheme')) + if (scheme !== DECODE_DATA) throw new Error(i18n.t('topicFilter.error.schema.noSchemeData')) + + const options = mediaTypes.split(DECODE_MEDIA_TYPES_SEPARATOR) + if (!options.includes(MIMETYPE_JSON)) throw new Error(i18n.t('topicFilter.error.schema.noJsonSchemaMimeType')) + if (!options.includes(DECODE_BASE64)) throw new Error(i18n.t('topicFilter.error.schema.noBase64MediaType')) + + try { + const decoded = atob(data) + const json: RJSFSchema = JSON.parse(decoded) + + // This will take care of some of the basic json error but not of a valid JSONSchema + const validate = validator.ajv.compile(json) + if (validate.errors?.length) throw new Error(i18n.t('topicFilter.error.schema.ajvValidationFails')) + // const valid = validate({}) + + const { properties } = json + + if (!properties) throw new Error(i18n.t('topicFilter.error.schema.ajvNoProperties')) + + return { mimeType: MIMETYPE_JSON, options, body: json } as UriInfo + } catch (error) { + if (error instanceof SyntaxError) throw new Error(i18n.t('topicFilter.error.schema.noBase64Data')) + if (error instanceof DOMException) throw new Error(i18n.t('topicFilter.error.schema.noJSON')) + if (error instanceof Error) throw new Error(error.message) + } +} From fb8215aeae5661748d6edf21b8b46e2890006677 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:08:53 +0000 Subject: [PATCH 03/22] feat(28664): redesign the schema manager --- .../TopicFilters/components/TopicSchemaDrawer.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx index 3cd46e9dc..568e8e396 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx @@ -19,7 +19,7 @@ import { import { TopicFilter } from '@/api/__generated__' import { Topic } from '@/components/MQTT/EntityTag.tsx' -import SchemaManager from '@/modules/TopicFilters/components/SchemaManager.tsx' +import TopicSchemaManager from '@/modules/TopicFilters/components/TopicSchemaManager.tsx' interface TopicSchemaDrawerProps { topicFilter: TopicFilter @@ -39,7 +39,7 @@ const TopicSchemaDrawer: FC = ({ topicFilter, trigger }) return ( <> {trigger(props)} - + @@ -48,12 +48,12 @@ const TopicSchemaDrawer: FC = ({ topicFilter, trigger }) - + - + Topic Filter: - + From 6d58aff18f0e0f35eb7a40c43aab80c5a165df33 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:34:18 +0000 Subject: [PATCH 04/22] feat(28664): add schema validation --- .../frontend/src/locales/en/translation.json | 6 +++ .../TopicFilters/utils/topic-filter.schema.ts | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 3942e3221..89a1af92c 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1030,6 +1030,12 @@ "header": "Manage the schemas of the topic filter", "submit": { "label": "OK" + }, + "status": { + "success": "Your topic filter is currently assigned a valid schema", + "missing": "Your topic filter is currently not assigned a schema", + "invalid": "Your topic filter is currently assigned a schema that is not valid", + "internalError": "Internal error - cannot extract your schema" } }, "error": { diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts index a62c7f195..3a6c8cb96 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts @@ -4,6 +4,8 @@ import { Accept } from 'react-dropzone' import validator from '@rjsf/validator-ajv8' import i18n from '@/config/i18n.config.ts' +import type { JSONSchema7 } from 'json-schema' +import type { AlertStatus } from '@chakra-ui/react' export const MIMETYPE_JSON = 'application/json' export const MIMETYPE_JSON_SCHEMA = 'application/schema+json' @@ -54,3 +56,38 @@ export const decodeDataUriJsonSchema = (dataUrl: string) => { if (error instanceof Error) throw new Error(error.message) } } + +export interface SchemaHandler { + schema?: JSONSchema7 + error?: string + status: AlertStatus + message: string +} + +export const validateSchemaFromDataURI = (topicFilterSchema: string | undefined): SchemaHandler => { + if (!topicFilterSchema) + return { + status: 'warning', + message: i18n.t('topicFilter.schema.status.missing'), + } + try { + const schema = decodeDataUriJsonSchema(topicFilterSchema) + if (!schema?.body) + return { + error: 'no body from the base64 payload', + status: 'error', + message: i18n.t('topicFilter.schema.status.internalError'), + } + return { + schema: schema.body, + status: 'success', + message: i18n.t('topicFilter.schema.status.success'), + } + } catch (e) { + return { + error: (e as Error).message, + status: 'error', + message: i18n.t('topicFilter.schema.status.success'), + } + } +} From b2f768797dad17fc2759b1c4abcf5d75af2ce3c0 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:41:47 +0000 Subject: [PATCH 05/22] feat(28664): add schema manager for topic filters --- .../components/TopicSchemaManager.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx new file mode 100644 index 000000000..b7f71bc36 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx @@ -0,0 +1,95 @@ +import { FC, useMemo } from 'react' +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, + VStack, +} from '@chakra-ui/react' + +import { TopicFilter } from '@/api/__generated__' + +import JsonSchemaBrowser from '@/components/rjsf/MqttTransformation/JsonSchemaBrowser.tsx' +import ErrorMessage from '@/components/ErrorMessage.tsx' +import SchemaUploader from '@/modules/TopicFilters/components/SchemaUploader.tsx' +import SchemaManager from '@/modules/TopicFilters/components/SchemaManager.tsx' +import { useTopicFilterManager } from '@/modules/TopicFilters/hooks/useTopicFilterManager.ts' +import { SchemaHandler, validateSchemaFromDataURI } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' +import { useTranslation } from 'react-i18next' + +interface CurrentSchemaProps { + topicFilter: TopicFilter +} + +const TopicSchemaManager: FC = ({ topicFilter }) => { + const { t } = useTranslation() + const schemaHandler = useMemo( + () => validateSchemaFromDataURI(topicFilter.schema), + [topicFilter.schema] + ) + const { onUpdate } = useTopicFilterManager() + + const onHandleUpload = (dataUri: string) => { + onUpdate(topicFilter.topicFilter, { ...topicFilter, schema: dataUri }) + } + + return ( + + + + + + {t('topicFilter.schema.prompt')} + + + + + {t('topicFilter.schema.tabs.current')} + {t('topicFilter.schema.tabs.upload')} + {t('topicFilter.schema.tabs.infer')} + + + + + {schemaHandler.schema && ( + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default TopicSchemaManager From e3a66bcb5d96a45804758993b261f309c8cd5c7b Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:52:32 +0000 Subject: [PATCH 06/22] feat(28664): add uploader for schemas --- .../components/SchemaUploader.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx new file mode 100644 index 000000000..b1756301e --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx @@ -0,0 +1,74 @@ +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDropzone } from 'react-dropzone' +import { AlertStatus, Button, Text, useToast, VStack } from '@chakra-ui/react' + +import { DEFAULT_TOAST_OPTION } from '@/hooks/useEdgeToast/toast-utils.ts' +import { getDropZoneBorder } from '@/modules/Theme/utils.ts' +import { ACCEPT_JSON_SCHEMA } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' + +interface SchemaUploaderProps { + id?: string + onUpload: (s: string) => void +} + +const SchemaUploader: FC = ({ onUpload }) => { + const [loading, setLoading] = useState(false) + const { t } = useTranslation() + const toast = useToast() + + const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ + noClick: true, + noKeyboard: true, + maxFiles: 1, + accept: ACCEPT_JSON_SCHEMA, + onDropRejected: (fileRejections) => { + const status: AlertStatus = 'error' + setLoading(false) + fileRejections.forEach((fileRejection) => { + toast({ + ...DEFAULT_TOAST_OPTION, + status, + title: t('rjsf.batchUpload.dropZone.status', { + ns: 'components', + context: status, + fileName: fileRejection.file.name, + }), + description: fileRejection.errors[0].message, + }) + }) + }, + onDropAccepted: async (files) => { + const [file] = files + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => { + if (typeof reader.result === 'string') onUpload(reader.result as string) + } + }, + }) + + return ( + + + {isDragActive && {t('rjsf.batchUpload.dropZone.dropping', { ns: 'components' })}} + {loading && {t('rjsf.batchUpload.dropZone.loading', { ns: 'components' })}} + {!isDragActive && !loading && ( + <> + {t('topicFilter.schema.actions.upload')} + + + )} + + ) +} + +export default SchemaUploader From edb03de9bdf695905a56076399bbf7d41db6c69d Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 12:52:42 +0000 Subject: [PATCH 07/22] feat(28664): update translations --- .../src/frontend/src/locales/en/translation.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 89a1af92c..2839243b2 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1028,6 +1028,17 @@ }, "schema": { "header": "Manage the schemas of the topic filter", + "prompt": "You can upload a schema or try to automatically infer a schema from the MQTT traffic on your installation", + "actions": { + "remove": "Remove assigned schema", + "assign": "Assign schema", + "upload": "Upload a JSON-Schema file" + }, + "tabs": { + "current": "Current Schema", + "upload": "Upload a new schema", + "infer": "Infer a schema" + }, "submit": { "label": "OK" }, From 1eb31773bdd0427362da775248bfb88680aabd8d Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 13:13:19 +0000 Subject: [PATCH 08/22] refactor(28664): refactor validation mark --- .../components/SchemaValidationMark.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.tsx index 3bdc6cb69..45bc9886f 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.tsx @@ -1,23 +1,21 @@ import { FC, useMemo } from 'react' -import { Alert, AlertIcon, Spinner } from '@chakra-ui/react' +import { Alert, AlertIcon } from '@chakra-ui/react' import { TopicFilter } from '@/api/__generated__' -import { useSamplingForTopic } from '@/api/hooks/useDomainModel/useSamplingForTopic.ts' +import { SchemaHandler, validateSchemaFromDataURI } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' interface SchemaValidationMarkProps { topicFilter: TopicFilter } const SchemaValidationMark: FC = ({ topicFilter }) => { - const { schema, isLoading } = useSamplingForTopic(topicFilter.topicFilter) - - const isSchemaValid = useMemo(() => { - return schema && Object.keys(schema).length !== 0 && schema.constructor === Object - }, [schema]) + const schemaHandler = useMemo( + () => validateSchemaFromDataURI(topicFilter.schema), + [topicFilter.schema] + ) - if (isLoading) return return ( - + ) From 20c5baf14ed7d857ef834fd9eaab084f7b001922 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 13:19:02 +0000 Subject: [PATCH 09/22] fix(28664): fix translations --- hivemq-edge/src/frontend/src/locales/en/translation.json | 5 +++++ .../src/modules/TopicFilters/hooks/useTopicFilterManager.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 2839243b2..ab9aecf12 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1071,6 +1071,11 @@ "description_success": "The Topic Filter has been deleted from the Edge", "description_error": "The Topic Filter could not be deleted" }, + "update": { + "title": "Update Topic Filter", + "description_success": "The Topic Filter has been updated on the Edge", + "description_error": "The Topic Filter could not be updated" + }, "updateCollection": { "title": "Edit the Topic Filters", "description_success": "The Topic Filters have been modified on the Edge", diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.ts index 23b616540..827d97397 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.ts @@ -56,15 +56,15 @@ export const useTopicFilterManager = () => { // TODO[NVL] Insert Edge-wide toast configuration (need refactoring) const formatToast = (operation: string) => ({ success: { - title: t(`device.drawer.tagList.toast.${operation}.title`), + title: t(`topicFilter.toast.${operation}.title`), description: t(`topicFilter.toast.${operation}.description`, { context: 'success' }), }, error: { - title: t(`device.drawer.tagList.toast.${operation}.title`), + title: t(`topicFilter.toast.${operation}.title`), description: t(`topicFilter.toast.${operation}.description`, { context: 'error' }), }, loading: { - title: t(`device.drawer.tagList.toast.${operation}.title`), + title: t(`topicFilter.toast.${operation}.title`), description: t('topicFilter.toast.description', { context: 'loading' }), }, }) From 6ecdf9dd6ed08388547f9c92176780288a4baaf3 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 14:19:32 +0000 Subject: [PATCH 10/22] test(28664): add mocks --- .../src/api/hooks/useTopicFilters/__handlers__/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/__handlers__/index.ts b/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/__handlers__/index.ts index 870a4f6ca..2c8a85bd9 100644 --- a/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/__handlers__/index.ts +++ b/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/__handlers__/index.ts @@ -1,9 +1,14 @@ import { http, HttpResponse } from 'msw' import { type TopicFilter, type TopicFilterList } from '@/api/__generated__' +export const MOCK_TOPIC_FILTER_SCHEMA_INVALID = 'data:application/json;base64,ewogICJ0ZXN0IjogMQp9Cg==' +export const MOCK_TOPIC_FILTER_SCHEMA_VALID = + 'data:application/json;base64,ewogICJ0eXBlIjogIm9iamVjdCIsCiAgInRpdGxlIjogIlRoaXMgaXMgYSBzaW1wbGUgc2NoZW1hIiwKICAicHJvcGVydGllcyI6IHsKICAgICJkZXNjcmlwdGlvbiI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgInRpdGxlIjogImRlc2NyaXB0aW9uIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlRoZSBkZXNjcmlwdGlvbiBvZiB0aGUgaXRlbSIKICAgIH0sCiAgICAibmFtZSI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgInRpdGxlIjogIm5hbWUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiVGhlIG5hbWUgb2YgdGhlIGl0ZW0iCiAgICB9CiAgfSwKICAicmVxdWlyZWQiOiBbCiAgICAiZGVzY3JpcHRpb24iLAogICAgIm5hbWUiCiAgXQp9Cg==' + export const MOCK_TOPIC_FILTER: TopicFilter = { topicFilter: 'a/topic/+/filter', description: 'This is a topic filter', + schema: MOCK_TOPIC_FILTER_SCHEMA_VALID, } export const handlers = [ From ece1a3643156c79fd1c8631bd50e160b1198a58e Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 14:19:42 +0000 Subject: [PATCH 11/22] test(28664): fix tests --- .../TopicFilters/TopicFilterManager.spec.cy.tsx | 15 +++------------ .../components/SchemaValidationMark.spec.cy.tsx | 7 ------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.spec.cy.tsx index f9cd58e04..a0a10e07e 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/TopicFilterManager.spec.cy.tsx @@ -1,6 +1,5 @@ import TopicFilterManager from '@/modules/TopicFilters/TopicFilterManager.tsx' -import { MOCK_TOPIC_FILTER } from '@/api/hooks/useTopicFilters/__handlers__' -import { GENERATE_DATA_MODELS } from '@/api/hooks/useDomainModel/__handlers__' +import { MOCK_TOPIC_FILTER, MOCK_TOPIC_FILTER_SCHEMA_INVALID } from '@/api/hooks/useTopicFilters/__handlers__' describe('TopicFilterManager', () => { beforeEach(() => { @@ -11,18 +10,10 @@ describe('TopicFilterManager', () => { { topicFilter: 'another/filter', description: 'This is a topic filter', + schema: MOCK_TOPIC_FILTER_SCHEMA_INVALID, }, ], }).as('getTopicFilters') - - cy.intercept('/api/v1/management/sampling/schema/**', (req) => { - if (req.url.includes(btoa('another/filter'))) req.reply({ statusCode: 400 }) - else req.reply(GENERATE_DATA_MODELS(true, req.query.topics as string)) - }) - - cy.intercept('/api/v1/management/sampling/topic/**', (req) => { - req.reply({ items: [] }) - }) }) // TODO[NVL] There is a problem with re-rendering in Cypress. Redesign the component @@ -45,7 +36,7 @@ describe('TopicFilterManager', () => { cy.get('@body').find('td').eq(0).should('have.text', 'a / topic / + / filter') cy.get('@body').find('td').eq(1).should('have.text', 'This is a topic filter') cy.get('@body').find('td').eq(2).children().should('have.attr', 'data-status', 'success') - cy.get('@body').find('td').eq(6).children().should('have.attr', 'data-status', 'success') + cy.get('@body').find('td').eq(6).children().should('have.attr', 'data-status', 'error') cy.get('@body').find('td').eq(3).find('button').as('topicActions') diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.spec.cy.tsx index f92ea8e19..071bdf879 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaValidationMark.spec.cy.tsx @@ -1,21 +1,14 @@ import { MOCK_TOPIC_FILTER } from '@/api/hooks/useTopicFilters/__handlers__' -import { GENERATE_DATA_MODELS } from '@/api/hooks/useDomainModel/__handlers__' import SchemaValidationMark from '@/modules/TopicFilters/components/SchemaValidationMark.tsx' describe('SchemaValidationMark', () => { beforeEach(() => { cy.viewport(800, 800) - cy.intercept('/api/v1/management/sampling/topic/**', { items: [] }).as('getTopic') - - cy.intercept('/api/v1/management/sampling/schema/**', GENERATE_DATA_MODELS(true, MOCK_TOPIC_FILTER.topicFilter)).as( - 'getSchema' - ) }) it('should render properly', () => { cy.mountWithProviders() - cy.getByTestId('validation-loading').should('be.visible') cy.get('[role="alert"]').should('have.attr', 'data-status', 'success') }) }) From ce7d845152ff4cef13213a9c253a5fc9605019c9 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 14:33:25 +0000 Subject: [PATCH 12/22] test(28664): add tests --- .../utils/topic-filter.schema.spec.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts index cc1417157..860438a72 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts @@ -1,5 +1,10 @@ import { describe, expect } from 'vitest' -import { decodeDataUriJsonSchema } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' +import { + decodeDataUriJsonSchema, + SchemaHandler, + validateSchemaFromDataURI, +} from '@/modules/TopicFilters/utils/topic-filter.schema.ts' +import { MOCK_TOPIC_FILTER_SCHEMA_VALID } from '@/api/hooks/useTopicFilters/__handlers__' interface Suite { data: string @@ -26,10 +31,33 @@ describe('decodeDataUriJsonSchema', () => { }) it('should return a valid json object', () => { + expect(decodeDataUriJsonSchema(MOCK_TOPIC_FILTER_SCHEMA_VALID)).toBeFalsy + }) +}) + +describe('validateSchemaFromDataURI', () => { + it('should return a warning if not assigned', () => { + expect(validateSchemaFromDataURI(undefined)).toStrictEqual({ + status: 'warning', + message: expect.stringContaining(''), + }) + }) + + it('should return an error', () => { expect( - decodeDataUriJsonSchema( - 'data:application/json;base64,IHsKICAgICJ0eXBlIiA6ICJvYmplY3QiLAogICAgInByb3BlcnRpZXMiIDogewogICAgICAiZGVmaW5pdGlvbiIgOiB7CiAgICAgICAgInR5cGUiIDogIm9iamVjdCIsCiAgICAgICAgInByb3BlcnRpZXMiIDogewogICAgICAgICAgImRhdGFUeXBlIiA6IHsKICAgICAgICAgICAgInR5cGUiIDogInN0cmluZyIsCiAgICAgICAgICAgICJlbnVtIiA6IFsgIk5VTEwiLCAiQk9PTCIsICJCWVRFIiwgIldPUkQiLCAiRFdPUkQiLCAiTFdPUkQiLCAiVVNJTlQiLCAiVUlOVCIsICJVRElOVCIsICJVTElOVCIsICJTSU5UIiwgIklOVCIsICJESU5UIiwgIkxJTlQiLCAiUkVBTCIsICJMUkVBTCIsICJDSEFSIiwgIldDSEFSIiwgIlNUUklORyIsICJXU1RSSU5HIiwgIlRJTUUiLCAiTFRJTUUiLCAiREFURSIsICJMREFURSIsICJUSU1FX09GX0RBWSIsICJMVElNRV9PRl9EQVkiLCAiREFURV9BTkRfVElNRSIsICJMREFURV9BTkRfVElNRSIsICJSQVdfQllURV9BUlJBWSIgXSwKICAgICAgICAgICAgInRpdGxlIiA6ICJEYXRhIFR5cGUiLAogICAgICAgICAgICAiZGVzY3JpcHRpb24iIDogIlRoZSBleHBlY3RlZCBkYXRhIHR5cGUgb2YgdGhlIHRhZyIsCiAgICAgICAgICAgICJlbnVtTmFtZXMiIDogWyAiTnVsbCIsICJCb29sZWFuIiwgIkJ5dGUiLCAiV29yZCAodW5pdCAxNikiLCAiRFdvcmQgKHVpbnQgMzIpIiwgIkxXb3JkICh1aW50IDY0KSIsICJVU2ludCAodWludCA4KSIsICJVaW50ICh1aW50IDE2KSIsICJVRGludCAodWludCAzMikiLCAiVUxpbnQgKHVpbnQgNjQpIiwgIlNpbnQgKGludCA4KSIsICJJbnQgKGludCAxNikiLCAiRGludCAoaW50IDMyKSIsICJMaW50IChpbnQgNjQpIiwgIlJlYWwgKGZsb2F0IDMyKSIsICJMUmVhbCAoZG91YmxlIDY0KSIsICJDaGFyICgxIGJ5dGUgY2hhcikiLCAiV0NoYXIgKDIgYnl0ZSBjaGFyKSIsICJTdHJpbmciLCAiV1N0cmluZyIsICJUaW1pbmcgKER1cmF0aW9uIG1zKSIsICJMb25nIFRpbWluZyAoRHVyYXRpb24gbnMpIiwgIkRhdGUgKERhdGVTdGFtcCkiLCAiTG9uZyBEYXRlIChEYXRlU3RhbXApIiwgIlRpbWUgT2YgRGF5IChUaW1lU3RhbXApIiwgIkxvbmcgVGltZSBPZiBEYXkgKFRpbWVTdGFtcCkiLCAiRGF0ZSBUaW1lIChEYXRlVGltZVN0YW1wKSIsICJMb25nIERhdGUgVGltZSAoRGF0ZVRpbWVTdGFtcCkiLCAiUmF3IEJ5dGUgQXJyYXkiIF0KICAgICAgICAgIH0sCiAgICAgICAgICAidGFnQWRkcmVzcyIgOiB7CiAgICAgICAgICAgICJ0eXBlIiA6ICJzdHJpbmciLAogICAgICAgICAgICAidGl0bGUiIDogIlRhZyBBZGRyZXNzIiwKICAgICAgICAgICAgImRlc2NyaXB0aW9uIiA6ICJUaGUgd2VsbCBmb3JtZWQgYWRkcmVzcyBvZiB0aGUgdGFnIHRvIHJlYWQiCiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICAicmVxdWlyZWQiIDogWyAiZGF0YVR5cGUiLCAidGFnQWRkcmVzcyIgXSwKICAgICAgICAidGl0bGUiIDogImRlZmluaXRpb24iLAogICAgICAgICJkZXNjcmlwdGlvbiIgOiAiVGhlIGFjdHVhbCBkZWZpbml0aW9uIG9mIHRoZSB0YWcgb24gdGhlIGRldmljZSIKICAgICAgfSwKICAgICAgImRlc2NyaXB0aW9uIiA6IHsKICAgICAgICAidHlwZSIgOiAic3RyaW5nIiwKICAgICAgICAidGl0bGUiIDogImRlc2NyaXB0aW9uIiwKICAgICAgICAiZGVzY3JpcHRpb24iIDogIkEgaHVtYW4gcmVhZGFibGUgZGVzY3JpcHRpb24gb2YgdGhlIHRhZyIKICAgICAgfSwKICAgICAgIm5hbWUiIDogewogICAgICAgICJ0eXBlIiA6ICJzdHJpbmciLAogICAgICAgICJ0aXRsZSIgOiAibmFtZSIsCiAgICAgICAgImRlc2NyaXB0aW9uIiA6ICJuYW1lIG9mIHRoZSB0YWcgdG8gYmUgdXNlZCBpbiBtYXBwaW5ncyIKICAgICAgfQogICAgfSwKICAgICJyZXF1aXJlZCIgOiBbICJkZWZpbml0aW9uIiwgImRlc2NyaXB0aW9uIiwgIm5hbWUiIF0KICB9Cg==' - ) - ).toBeFalsy + validateSchemaFromDataURI('data:application/json;base64,ewogICJ0ZXN0IjogMQp9Cg==') + ).toStrictEqual({ + error: 'Not a valid JSONSchema: `properties` is missing', + message: expect.stringContaining(''), + status: 'error', + }) + }) + + it('should return a schema', () => { + expect(validateSchemaFromDataURI(MOCK_TOPIC_FILTER_SCHEMA_VALID)).toStrictEqual({ + message: expect.stringContaining(''), + status: 'success', + schema: expect.objectContaining({}), + }) }) }) From 5a20d8fd2cf2fbb4bbee16fa3d83fc837ab6250a Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 14:57:42 +0000 Subject: [PATCH 13/22] test(28664): fix tests --- .../src/api/hooks/useTopicFilters/useListTopicFilters.spec.ts | 3 ++- .../modules/TopicFilters/hooks/useTopicFilterManager.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/useListTopicFilters.spec.ts b/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/useListTopicFilters.spec.ts index e819957ee..7b9d83656 100644 --- a/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/useListTopicFilters.spec.ts +++ b/hivemq-edge/src/frontend/src/api/hooks/useTopicFilters/useListTopicFilters.spec.ts @@ -5,7 +5,7 @@ import { server } from '@/__test-utils__/msw/mockServer.ts' import { SimpleWrapper as wrapper } from '@/__test-utils__/hooks/SimpleWrapper.tsx' import type { TopicFilterList } from '@/api/__generated__' import { useListTopicFilters } from '@/api/hooks/useTopicFilters/useListTopicFilters.ts' -import { handlers } from '@/api/hooks/useTopicFilters/__handlers__' +import { handlers, MOCK_TOPIC_FILTER_SCHEMA_VALID } from '@/api/hooks/useTopicFilters/__handlers__' describe('useListTopicFilters', () => { beforeEach(() => { @@ -27,6 +27,7 @@ describe('useListTopicFilters', () => { { description: 'This is a topic filter', topicFilter: 'a/topic/+/filter', + schema: MOCK_TOPIC_FILTER_SCHEMA_VALID, }, ], }) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.spec.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.spec.ts index 87d3e7b96..9b27e86c4 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.spec.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/hooks/useTopicFilterManager.spec.ts @@ -4,7 +4,7 @@ import { renderHook, waitFor } from '@testing-library/react' import { server } from '@/__test-utils__/msw/mockServer.ts' import { SimpleWrapper as wrapper } from '@/__test-utils__/hooks/SimpleWrapper.tsx' import { useTopicFilterManager } from '@/modules/TopicFilters/hooks/useTopicFilterManager.ts' -import { handlers } from '@/api/hooks/useTopicFilters/__handlers__' +import { handlers, MOCK_TOPIC_FILTER_SCHEMA_VALID } from '@/api/hooks/useTopicFilters/__handlers__' import '@/config/i18n.config.ts' @@ -27,6 +27,7 @@ describe('useTopicFilterManager', () => { { description: 'This is a topic filter', topicFilter: 'a/topic/+/filter', + schema: MOCK_TOPIC_FILTER_SCHEMA_VALID, }, ], }, From caf474760e13d0c99e856a84817e7f5605c34211 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 15:04:33 +0000 Subject: [PATCH 14/22] test(28664): add tests --- .../components/SchemaUploader.spec.cy.tsx | 16 ++++++++++++++++ .../TopicFilters/components/SchemaUploader.tsx | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.spec.cy.tsx diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.spec.cy.tsx new file mode 100644 index 000000000..9c0237b40 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.spec.cy.tsx @@ -0,0 +1,16 @@ +import SchemaUploader from '@/modules/TopicFilters/components/SchemaUploader.tsx' + +describe('SchemaUploader', () => { + beforeEach(() => { + cy.viewport(800, 800) + }) + + it('should render properly', () => { + const onUpload = cy.stub().as('onUpload') + cy.mountWithProviders() + + cy.get('#dropzone').as('dropzone') + cy.get('#dropzone p').should('have.text', 'Upload a JSON-Schema file') + cy.get('#dropzone button').should('have.text', 'Select file') + }) +}) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx index b1756301e..a8060724f 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx @@ -8,7 +8,6 @@ import { getDropZoneBorder } from '@/modules/Theme/utils.ts' import { ACCEPT_JSON_SCHEMA } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' interface SchemaUploaderProps { - id?: string onUpload: (s: string) => void } @@ -58,7 +57,7 @@ const SchemaUploader: FC = ({ onUpload }) => { alignItems="center" id="dropzone" > - + {isDragActive && {t('rjsf.batchUpload.dropZone.dropping', { ns: 'components' })}} {loading && {t('rjsf.batchUpload.dropZone.loading', { ns: 'components' })}} {!isDragActive && !loading && ( From 3fdf4b27a630dd42e5ae6eb84965f11f019016b5 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Mon, 9 Dec 2024 17:23:53 +0000 Subject: [PATCH 15/22] feat(28664): add remove schema --- .../TopicFilters/components/TopicSchemaManager.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx index b7f71bc36..40b9debcb 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx @@ -40,6 +40,10 @@ const TopicSchemaManager: FC = ({ topicFilter }) => { onUpdate(topicFilter.topicFilter, { ...topicFilter, schema: dataUri }) } + const onHandleClear = () => { + onUpdate(topicFilter.topicFilter, { ...topicFilter, schema: undefined }) + } + return ( @@ -64,7 +68,9 @@ const TopicSchemaManager: FC = ({ topicFilter }) => { - + )} From 646be782e240186284868c0f3d2ad02ce0a54511 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:29:44 +0000 Subject: [PATCH 16/22] feat(28664): add data-url encoding --- .../src/modules/TopicFilters/utils/topic-filter.schema.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts index 3a6c8cb96..b524a68a2 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts @@ -57,6 +57,10 @@ export const decodeDataUriJsonSchema = (dataUrl: string) => { } } +export const encodeDataUriJsonSchema = (schema: RJSFSchema) => { + return `data:${MIMETYPE_JSON};base64,${btoa(JSON.stringify(schema))}` +} + export interface SchemaHandler { schema?: JSONSchema7 error?: string From f02561d3fe46e11a0bf0a120076272f5d034f9f0 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:29:58 +0000 Subject: [PATCH 17/22] fix(28664): validation --- .../utils/topic-filter.schema.spec.ts | 24 ++++++++----------- .../TopicFilters/utils/topic-filter.schema.ts | 12 +++++----- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts index 860438a72..a5328c80c 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.spec.ts @@ -1,6 +1,7 @@ import { describe, expect } from 'vitest' import { decodeDataUriJsonSchema, + encodeDataUriJsonSchema, SchemaHandler, validateSchemaFromDataURI, } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' @@ -20,10 +21,6 @@ describe('decodeDataUriJsonSchema', () => { { data: 'data:test,456', error: "The media types doesn't include the mandatory `application/schema+json`" }, { data: 'data:application/json,456', error: "The media types doesn't include the mandatory `base64`" }, { data: 'data:application/json;base64,456', error: 'The data is not properly encoded as a `base64` string' }, - { - data: 'data:application/json;base64,ewogICJ0ZXN0IjogMQp9Cg==', - error: 'Not a valid JSONSchema: `properties` is missing', - }, ] it.each(tests)('$data should throw $error', ({ data, error }) => { @@ -35,6 +32,15 @@ describe('decodeDataUriJsonSchema', () => { }) }) +describe('encodeDataUriJsonSchema', () => { + it('should return a data-url', () => { + const schema = { test: 1 } + expect(encodeDataUriJsonSchema(schema)).toStrictEqual( + `data:application/json;base64,${btoa(JSON.stringify(schema))}` + ) + }) +}) + describe('validateSchemaFromDataURI', () => { it('should return a warning if not assigned', () => { expect(validateSchemaFromDataURI(undefined)).toStrictEqual({ @@ -43,16 +49,6 @@ describe('validateSchemaFromDataURI', () => { }) }) - it('should return an error', () => { - expect( - validateSchemaFromDataURI('data:application/json;base64,ewogICJ0ZXN0IjogMQp9Cg==') - ).toStrictEqual({ - error: 'Not a valid JSONSchema: `properties` is missing', - message: expect.stringContaining(''), - status: 'error', - }) - }) - it('should return a schema', () => { expect(validateSchemaFromDataURI(MOCK_TOPIC_FILTER_SCHEMA_VALID)).toStrictEqual({ message: expect.stringContaining(''), diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts index b524a68a2..cc0b256ac 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/utils/topic-filter.schema.ts @@ -41,19 +41,19 @@ export const decodeDataUriJsonSchema = (dataUrl: string) => { const json: RJSFSchema = JSON.parse(decoded) // This will take care of some of the basic json error but not of a valid JSONSchema - const validate = validator.ajv.compile(json) - if (validate.errors?.length) throw new Error(i18n.t('topicFilter.error.schema.ajvValidationFails')) - // const valid = validate({}) + validator.ajv.compile(json) + // TODO[NVL] We need to decide what we want to require on the schema const { properties } = json - if (!properties) throw new Error(i18n.t('topicFilter.error.schema.ajvNoProperties')) return { mimeType: MIMETYPE_JSON, options, body: json } as UriInfo } catch (error) { if (error instanceof SyntaxError) throw new Error(i18n.t('topicFilter.error.schema.noBase64Data')) if (error instanceof DOMException) throw new Error(i18n.t('topicFilter.error.schema.noJSON')) - if (error instanceof Error) throw new Error(error.message) + if (error instanceof Error) { + throw new Error(`${error.message}`) + } } } @@ -91,7 +91,7 @@ export const validateSchemaFromDataURI = (topicFilterSchema: string | undefined) return { error: (e as Error).message, status: 'error', - message: i18n.t('topicFilter.schema.status.success'), + message: i18n.t('topicFilter.schema.status.invalid'), } } } From 285a955326bb8eb094d4ff527e6a87abd05e6473 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:31:21 +0000 Subject: [PATCH 18/22] refactor(28664): refactor the sampler and add assign CTA --- ....spec.cy.tsx => SchemaSampler.spec.cy.tsx} | 18 ++++++++------- .../{SchemaManager.tsx => SchemaSampler.tsx} | 23 ++++++++++++++++--- 2 files changed, 30 insertions(+), 11 deletions(-) rename hivemq-edge/src/frontend/src/modules/TopicFilters/components/{SchemaManager.spec.cy.tsx => SchemaSampler.spec.cy.tsx} (67%) rename hivemq-edge/src/frontend/src/modules/TopicFilters/components/{SchemaManager.tsx => SchemaSampler.tsx} (58%) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.spec.cy.tsx similarity index 67% rename from hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.spec.cy.tsx rename to hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.spec.cy.tsx index 5368837b2..1b4491f68 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.spec.cy.tsx @@ -1,13 +1,10 @@ import { MOCK_TOPIC_FILTER } from '@/api/hooks/useTopicFilters/__handlers__' import { GENERATE_DATA_MODELS } from '@/api/hooks/useDomainModel/__handlers__' -import SchemaManager from '@/modules/TopicFilters/components/SchemaManager.tsx' +import SchemaSampler from '@/modules/TopicFilters/components/SchemaSampler.tsx' -describe('SchemaManager', () => { +describe('SchemaSampler', () => { beforeEach(() => { cy.viewport(800, 800) - // cy.intercept('/api/v1/management/domain/topics/schema?*', (req) => { - // req.reply(GENERATE_DATA_MODELS(true, req.query.topics as string)) - // }) }) it.skip('should render loading errors', () => { @@ -15,7 +12,7 @@ describe('SchemaManager', () => { statusCode: 404, body: { title: 'The schema for the tags cannot be found', status: 404 }, }) - cy.mountWithProviders() + cy.mountWithProviders() cy.getByTestId('loading-spinner') cy.get('[role="alert"]').should('have.attr', 'data-status', 'error').should('have.text', 'Not Found') @@ -24,7 +21,7 @@ describe('SchemaManager', () => { it.skip('should render validation errors', () => { cy.intercept('/api/v1/management/sampling/topic/**', { items: [] }) cy.intercept('/api/v1/management/sampling/schema/**', {}).as('getSchema') - cy.mountWithProviders() + cy.mountWithProviders() cy.getByTestId('loading-spinner') cy.get('[role="alert"]') @@ -37,10 +34,15 @@ describe('SchemaManager', () => { cy.intercept('/api/v1/management/sampling/schema/**', GENERATE_DATA_MODELS(true, MOCK_TOPIC_FILTER.topicFilter)).as( 'getSchema' ) - cy.mountWithProviders() + const onUpload = cy.stub().as('onUpload') + cy.mountWithProviders() cy.getByTestId('loading-spinner') cy.get('h4').should('contain.text', 'a/topic/+/filter') cy.get('[role="list"]').should('be.visible') + + cy.get('@onUpload').should('not.have.been.called') + cy.getByTestId('schema-sampler-upload').should('have.text', 'Assign schema').click() + cy.get('@onUpload').should('have.been.calledWithMatch', 'data:application/json;base64,') }) }) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.tsx similarity index 58% rename from hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.tsx rename to hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.tsx index 4bfd194f4..9128f9d15 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaManager.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaSampler.tsx @@ -1,18 +1,21 @@ import { FC, useMemo } from 'react' import { useTranslation } from 'react-i18next' import type { JSONSchema7 } from 'json-schema' +import { Button, Card, CardBody, CardFooter } from '@chakra-ui/react' import { TopicFilter } from '@/api/__generated__' import { useSamplingForTopic } from '@/api/hooks/useDomainModel/useSamplingForTopic.ts' import LoaderSpinner from '@/components/Chakra/LoaderSpinner.tsx' import ErrorMessage from '@/components/ErrorMessage.tsx' import JsonSchemaBrowser from '@/components/rjsf/MqttTransformation/JsonSchemaBrowser.tsx' +import { encodeDataUriJsonSchema } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' interface SchemaManagerProps { topicFilter: TopicFilter + onUpload: (s: string) => void } -const SchemaManager: FC = ({ topicFilter }) => { +const SchemaSampler: FC = ({ topicFilter, onUpload }) => { const { t } = useTranslation() const { schema, isLoading, isError, error } = useSamplingForTopic(topicFilter.topicFilter) @@ -24,7 +27,21 @@ const SchemaManager: FC = ({ topicFilter }) => { if (isError && error) return if (!isSchemaValid) return - return + return ( + + + + + + + + + ) } -export default SchemaManager +export default SchemaSampler From 771c454bec586ef3ed60b6bcb131e95f84ac03d4 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:31:34 +0000 Subject: [PATCH 19/22] refactor(28664): refactor uploader --- .../components/SchemaUploader.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx index a8060724f..bcfe7341f 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/SchemaUploader.tsx @@ -1,7 +1,7 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDropzone } from 'react-dropzone' -import { AlertStatus, Button, Text, useToast, VStack } from '@chakra-ui/react' +import { AlertStatus, Button, Card, CardBody, Text, useToast } from '@chakra-ui/react' import { DEFAULT_TOAST_OPTION } from '@/hooks/useEdgeToast/toast-utils.ts' import { getDropZoneBorder } from '@/modules/Theme/utils.ts' @@ -48,25 +48,28 @@ const SchemaUploader: FC = ({ onUpload }) => { }) return ( - - - {isDragActive && {t('rjsf.batchUpload.dropZone.dropping', { ns: 'components' })}} - {loading && {t('rjsf.batchUpload.dropZone.loading', { ns: 'components' })}} - {!isDragActive && !loading && ( - <> - {t('topicFilter.schema.actions.upload')} - - - )} - + + + + {isDragActive && {t('rjsf.batchUpload.dropZone.dropping', { ns: 'components' })}} + {loading && {t('rjsf.batchUpload.dropZone.loading', { ns: 'components' })}} + {!isDragActive && !loading && ( + <> + {t('topicFilter.schema.actions.upload')} + + + )} + + ) } From 45e350649c8c043f9163e8cd9bdf0844f6d9ef9f Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:32:13 +0000 Subject: [PATCH 20/22] fix(28664): fix translations --- hivemq-edge/src/frontend/src/locales/en/translation.json | 1 + .../src/modules/TopicFilters/components/TopicSchemaDrawer.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index ab9aecf12..2dc47eadf 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -1028,6 +1028,7 @@ }, "schema": { "header": "Manage the schemas of the topic filter", + "title": "Topic Filter:", "prompt": "You can upload a schema or try to automatically infer a schema from the MQTT traffic on your installation", "actions": { "remove": "Remove assigned schema", diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx index 568e8e396..19927f470 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaDrawer.tsx @@ -50,7 +50,8 @@ const TopicSchemaDrawer: FC = ({ topicFilter, trigger }) - Topic Filter: + {t('topicFilter.schema.title')}{' '} + From 74eeea4f0c35baff2eecbd27301cad7752d12edd Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 11:35:46 +0000 Subject: [PATCH 21/22] refactor(28664): refactor the schema manager - change the order of the tabs - reorganise the status messages --- .../components/TopicSchemaManager.tsx | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx index 40b9debcb..1ea0089d1 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx @@ -19,7 +19,7 @@ import { TopicFilter } from '@/api/__generated__' import JsonSchemaBrowser from '@/components/rjsf/MqttTransformation/JsonSchemaBrowser.tsx' import ErrorMessage from '@/components/ErrorMessage.tsx' import SchemaUploader from '@/modules/TopicFilters/components/SchemaUploader.tsx' -import SchemaManager from '@/modules/TopicFilters/components/SchemaManager.tsx' +import SchemaSampler from '@/modules/TopicFilters/components/SchemaSampler.tsx' import { useTopicFilterManager } from '@/modules/TopicFilters/hooks/useTopicFilterManager.ts' import { SchemaHandler, validateSchemaFromDataURI } from '@/modules/TopicFilters/utils/topic-filter.schema.ts' import { useTranslation } from 'react-i18next' @@ -46,7 +46,7 @@ const TopicSchemaManager: FC = ({ topicFilter }) => { return ( - + @@ -56,40 +56,32 @@ const TopicSchemaManager: FC = ({ topicFilter }) => { {t('topicFilter.schema.tabs.current')} - {t('topicFilter.schema.tabs.upload')} {t('topicFilter.schema.tabs.infer')} + {t('topicFilter.schema.tabs.upload')} - - {schemaHandler.schema && ( - - - - - - - - - )} - - - - - - - + {schemaHandler.error && ( + + )} + {schemaHandler.schema && } - + + + + + + + From 6700288151a49264c544b9f33a42c527e4995241 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Tue, 10 Dec 2024 12:21:05 +0000 Subject: [PATCH 22/22] fix(28664): show examples --- .../src/modules/TopicFilters/components/TopicSchemaManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx index 1ea0089d1..fc17c77f8 100644 --- a/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx +++ b/hivemq-edge/src/frontend/src/modules/TopicFilters/components/TopicSchemaManager.tsx @@ -67,7 +67,7 @@ const TopicSchemaManager: FC = ({ topicFilter }) => { {schemaHandler.error && ( )} - {schemaHandler.schema && } + {schemaHandler.schema && }