From bb6c924181e9ce34b528f040b3f65700ed1142dc Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Thu, 23 May 2024 11:47:45 +0300 Subject: [PATCH 01/13] fix: rss datasource mixed up --- src/api/api.test.ts | 33 +++++++ src/api/api.ts | 220 ++++++++++++++++++++++++-------------------- src/types.ts | 6 +- src/utils.test.ts | 41 +++++++++ src/utils.ts | 18 +++- 5 files changed, 214 insertions(+), 104 deletions(-) create mode 100644 src/utils.test.ts diff --git a/src/api/api.test.ts b/src/api/api.test.ts index 745f173..b97392e 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -416,5 +416,38 @@ describe('Api', () => { expect(result?.length).toEqual(1); expect(result[0].fields.length).toEqual(12); }); + + /** + * Source with different key count in item + */ + it('Should return null value if key doesn`t contain in element', async () => { + xmlResponse.data = ` + + + + RSS Tutorial + + Test.
]]>
+
+ +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + + const tagField = result[0].fields.find((elem) => elem.name === 'tag'); + + expect(result?.length).toEqual(1); + expect(tagField?.values).toEqual([null, 'RSS Tag 2']); + }); }); }); diff --git a/src/api/api.ts b/src/api/api.ts index a76ba06..8c405a0 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -5,7 +5,7 @@ import { lastValueFrom } from 'rxjs'; import { ALWAYS_ARRAY, FeedTypeValue, ItemKey, MetaProperties } from '../constants'; import { DataItem, DataSourceOptions, FeedItems, Query } from '../types'; -import { isDateBetweenRange, setItem } from '../utils'; +import { createUniqueKeyObject, isDateBetweenRange, setItem } from '../utils'; /** * API @@ -120,10 +120,14 @@ export class Api { } /** - * Find all items + * Configure Items + * Take all the unique keys in all items */ - const items: FeedItems = {}; + const items: FeedItems = createUniqueKeyObject(channel.item); + /** + * Find all items + */ channel.item.forEach((item: DataItem) => { /** * Filter by specified Date field @@ -132,44 +136,51 @@ export class Api { return; } - Object.keys(item).forEach((key: string) => { + Object.keys(items).forEach((key: string) => { + let currentKey = key; let value = item[key]; /** - * Parse Meta + * If Item doesn`t contain key set key with null value to avoid mixed up */ - if (key === ItemKey.META && (value as Record)['@_property'] === MetaProperties.OG_IMAGE) { - key = MetaProperties.OG_IMAGE; - value = (value as Record)['@_content']; - } - - /** - * Parse Guid - */ - if (key === ItemKey.GUID && (value as Record)['#text']) { - value = (value as Record)['#text']; - } - - /** - * Parse Encoded content for H4 and first Image - */ - if (key === ItemKey.CONTENT_ENCODED) { - const h4 = value.toString().match(/

(.*?)<\/h4>/); - const figure = value.toString().match(/
(.*?)<\/figure>/); + if (!value) { + setItem(items, currentKey, null); + } else { + /** + * Parse Meta + */ + if (key === ItemKey.META && (value as Record)['@_property'] === MetaProperties.OG_IMAGE) { + currentKey = MetaProperties.OG_IMAGE; + value = (value as Record)['@_content']; + } - setItem(items, ItemKey.CONTENT_H4, h4?.length ? h4[1] : ''); + /** + * Parse Guid + */ + if (key === ItemKey.GUID && (value as Record)['#text']) { + value = (value as Record)['#text']; + } /** - * Extract image and source + * Parse Encoded content for H4 and first Image */ - if (figure?.length) { - setItem(items, ItemKey.CONTENT_IMG, figure[1]); - const img = figure[1].match(//); - setItem(items, ItemKey.CONTENT_IMG_SRC, img?.length ? img[1] : ''); + if (key === ItemKey.CONTENT_ENCODED) { + const h4 = value.toString().match(/

(.*?)<\/h4>/); + const figure = value.toString().match(/
(.*?)<\/figure>/); + + setItem(items, ItemKey.CONTENT_H4, h4?.length ? h4[1] : ''); + + /** + * Extract image and source + */ + if (figure?.length) { + setItem(items, ItemKey.CONTENT_IMG, figure[1]); + const img = figure[1].match(//); + setItem(items, ItemKey.CONTENT_IMG_SRC, img?.length ? img[1] : ''); + } } + setItem(items, currentKey, value as string); } - - setItem(items, key, value as string); }); }); @@ -230,10 +241,14 @@ export class Api { } /** - * Find all entries + * Configure entries + * Take all the unique keys in all entries */ - const entries: FeedItems = {}; + const entries: FeedItems = createUniqueKeyObject(feed.entry); + /** + * Find all entries + */ feed.entry.forEach((entry: DataItem) => { /** * Filter by specified Date field @@ -242,91 +257,98 @@ export class Api { return; } - Object.keys(entry).forEach((key: string) => { + Object.keys(entries).forEach((key: string) => { let value = entry[key]; /** - * Link + * If Entry doesn`t contain key set key with null value to avoid mixed up */ - if (key === ItemKey.LINK && (value as Record)['@_href']) { - value = (value as Record)['@_href']; - } - - /** - * Content - */ - if (key === ItemKey.CONTENT && (value as Record)['#text']) { - value = (value as Record)['#text']; - } - - /** - * Summary - */ - if (key === ItemKey.SUMMARY && (value as Record)['#text']) { - value = (value as Record)['#text']; - } - - /** - * Author - */ - if (key === ItemKey.AUTHOR && (value as Record)['name']) { - value = (value as Record)['name']; - } + if (!value) { + setItem(entries, key, null); + } else { + /** + * Link + */ + if (key === ItemKey.LINK && (value as Record)['@_href']) { + value = (value as Record)['@_href']; + } - /** - * Thumbnail - */ - if (key === ItemKey.MEDIA_THUMBNAIL && (value as Record)['@_url']) { - value = (value as Record)['@_url']; - } + /** + * Content + */ + if (key === ItemKey.CONTENT && (value as Record)['#text']) { + value = (value as Record)['#text']; + } - /** - * Media Group - */ - if (key === ItemKey.MEDIA_GROUP) { - const mediaGroup: Record = value as Record; + /** + * Summary + */ + if (key === ItemKey.SUMMARY && (value as Record)['#text']) { + value = (value as Record)['#text']; + } /** - * Thumbnail URL + * Author */ - if ( - mediaGroup[ItemKey.MEDIA_THUMBNAIL] && - (mediaGroup[ItemKey.MEDIA_THUMBNAIL] as Record)['@_url'] - ) { - setItem( - entries, - `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_THUMBNAIL}:url`, - (mediaGroup[ItemKey.MEDIA_THUMBNAIL] as Record)['@_url'] as string - ); + if (key === ItemKey.AUTHOR && (value as Record)['name']) { + value = (value as Record)['name']; } /** - * Content URL + * Thumbnail */ - if ( - mediaGroup[ItemKey.MEDIA_CONTENT] && - (mediaGroup[ItemKey.MEDIA_CONTENT] as Record)['@_url'] - ) { - setItem( - entries, - `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_CONTENT}:url`, - (mediaGroup[ItemKey.MEDIA_CONTENT] as Record)['@_url'] as string - ); + if (key === ItemKey.MEDIA_THUMBNAIL && (value as Record)['@_url']) { + value = (value as Record)['@_url']; } /** - * Description + * Media Group */ - if (mediaGroup[ItemKey.MEDIA_DESCRIPTION]) { - setItem( - entries, - `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_DESCRIPTION}`, - mediaGroup[ItemKey.MEDIA_DESCRIPTION] as string - ); + if (key === ItemKey.MEDIA_GROUP) { + const mediaGroup: Record = value as Record; + + /** + * Thumbnail URL + */ + if ( + mediaGroup[ItemKey.MEDIA_THUMBNAIL] && + (mediaGroup[ItemKey.MEDIA_THUMBNAIL] as Record)['@_url'] + ) { + setItem( + entries, + `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_THUMBNAIL}:url`, + (mediaGroup[ItemKey.MEDIA_THUMBNAIL] as Record)['@_url'] as string + ); + } + + /** + * Content URL + */ + if ( + mediaGroup[ItemKey.MEDIA_CONTENT] && + (mediaGroup[ItemKey.MEDIA_CONTENT] as Record)['@_url'] + ) { + setItem( + entries, + `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_CONTENT}:url`, + (mediaGroup[ItemKey.MEDIA_CONTENT] as Record)['@_url'] as string + ); + } + + /** + * Description + */ + if (mediaGroup[ItemKey.MEDIA_DESCRIPTION]) { + setItem( + entries, + `${ItemKey.MEDIA_GROUP}:${ItemKey.MEDIA_DESCRIPTION}`, + mediaGroup[ItemKey.MEDIA_DESCRIPTION] as string + ); + } } - } - setItem(entries, key, value as string); + setItem(entries, key, value as string); + } }); }); diff --git a/src/types.ts b/src/types.ts index fd789cb..efd221a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,11 +44,11 @@ export interface DataSourceOptions extends DataSourceJsonData { */ export interface FeedItems { /** - * Mapping of ID to an array of strings representing the items + * Mapping of ID to an array of strings or strings and null * - * @type {Record} + * @type {Record} */ - [id: string]: string[]; + [id: string]: Array; } /** diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..0381926 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,41 @@ +import { createUniqueKeyObject } from './utils'; + +/** + * createUniqueKeyObject + */ +describe('createUniqueKeyObject', () => { + it('Returns an empty object for an empty input array', () => { + const result = createUniqueKeyObject([]); + expect(result).toEqual({}); + }); + + it('Creates a unique key object for a non-empty array', () => { + const items = [ + { name: 'John', age: 30 }, + { name: 'Jane', email: 'jane@example.com' }, + { name: 'Bob', age: 25, email: 'bob@example.com' }, + ]; + + const result = createUniqueKeyObject(items as any); + expect(result).toEqual({ + name: [], + age: [], + email: [], + }); + }); + + it('handles duplicate keys in the array', () => { + const items = [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 35 }, + { name: 'John', email: 'john@example.com' }, + ]; + + const result = createUniqueKeyObject(items as any); + expect(result).toEqual({ + name: [], + age: [], + email: [], + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index e2b0685..1c2471d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,9 @@ import { TimeRange } from '@grafana/data'; -import { FeedItems } from 'types'; +import { DataItem, FeedItems } from 'types'; /** * Set or added Item values */ -export const setItem = (items: FeedItems, key: string, value: string) => { +export const setItem = (items: FeedItems, key: string, value: string | null) => { items[key] ? items[key].push(value) : (items[key] = [value]); }; @@ -27,3 +27,17 @@ export const isDateBetweenRange = (value: string, range: TimeRange): boolean => return true; }; + +/** + * Сreate Unique Key Object + * Linear pass of items and forming an object with unique keys based on an array of objects + */ +export const createUniqueKeyObject = (items: DataItem[]) => + items.reduce((result: Record, obj) => { + Object.keys(obj).forEach((key) => { + if (!result[key]) { + result[key] = []; + } + }); + return result; + }, {}); From 439eb08bf086cf6c2826945a2c5b8ba8a3312622 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Thu, 23 May 2024 17:05:20 +0300 Subject: [PATCH 02/13] added new provisioning and data sources --- docker-compose.yml | 2 +- provisioning/dashboards/comparing.json | 457 +++++++++++++++++++++++ provisioning/datasources/datasource.yaml | 17 +- 3 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 provisioning/dashboards/comparing.json diff --git a/docker-compose.yml b/docker-compose.yml index a2e9573..c9cf146 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: environment: - GF_DEFAULT_APP_MODE=development - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/panels.json - - GF_INSTALL_PLUGINS=marcusolsson-dynamictext-panel + - GF_INSTALL_PLUGINS=marcusolsson-dynamictext-panel,yesoreyeram-infinity-datasource volumes: - ./dist:/var/lib/grafana/plugins/volkovlabs-rss-datasource - ./provisioning:/etc/grafana/provisioning diff --git a/provisioning/dashboards/comparing.json b/provisioning/dashboards/comparing.json new file mode 100644 index 0000000..84a6036 --- /dev/null +++ b/provisioning/dashboards/comparing.json @@ -0,0 +1,457 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 5, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "infinity1" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "", + "url": "${__data.fields.link}" + } + ] + }, + { + "id": "custom.width", + "value": 434 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "link" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Release Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "contact" + }, + "properties": [ + { + "id": "custom.width", + "value": 192 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "availability" + }, + "properties": [ + { + "id": "custom.width", + "value": 203 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Title" + }, + "properties": [ + { + "id": "custom.width", + "value": 514 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "tag" + }, + "properties": [ + { + "id": "noValue", + "value": "Blank" + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "pubDate" + } + ] + }, + "pluginVersion": "10.3.1", + "targets": [ + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "789789897" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "parser": "backend", + "refId": "A", + "root_selector": "rss.channel.item", + "source": "url", + "type": "xml", + "uql": "parse-xml", + "url": "https://grafana.com/docs/grafana-cloud/whats-new/index.xml", + "url_options": { + "data": "", + "method": "GET" + } + } + ], + "title": "Cloud What's New - Infinity", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "availability": true, + "content": true, + "description": true, + "documentationURL": true, + "guid": true, + "link": true, + "offering": true, + "pubDate": false, + "selfManagedEdition": true + }, + "includeByName": {}, + "indexByName": { + "availability": 0, + "content": 1, + "description": 2, + "documentationURL": 3, + "guid": 4, + "link": 5, + "offering": 6, + "pubDate": 9, + "selfManagedEdition": 10, + "tag": 8, + "title": 7 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "volkovlabs-rss-datasource", + "uid": "rss1" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "", + "url": "${__data.fields.link}" + } + ] + }, + { + "id": "custom.width", + "value": 434 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "link" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Release Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "contact" + }, + "properties": [ + { + "id": "custom.width", + "value": 192 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "availability" + }, + "properties": [ + { + "id": "custom.width", + "value": 203 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Title" + }, + "properties": [ + { + "id": "custom.width", + "value": 514 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "tag" + }, + "properties": [ + { + "id": "noValue", + "value": "Blank" + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "tag" + } + ] + }, + "pluginVersion": "10.3.1", + "targets": [ + { + "datasource": { + "type": "volkovlabs-rss-datasource", + "uid": "45645564" + }, + "feedType": "items", + "refId": "A" + } + ], + "title": "Cloud What's New - RSS Datasource", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "availability": true, + "content": true, + "description": true, + "documentationURL": true, + "guid": true, + "link": true, + "offering": true, + "pubDate": false, + "selfManagedEdition": true + }, + "includeByName": {}, + "indexByName": { + "availability": 7, + "content": 5, + "description": 6, + "documentationURL": 8, + "guid": 4, + "link": 2, + "offering": 9, + "pubDate": 3, + "selfManagedEdition": 10, + "tag": 1, + "title": 0 + }, + "renameByName": {} + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now/y", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Comparing Datasource", + "uid": "bdmc5fb3z0074c", + "version": 20, + "weekStart": "" +} diff --git a/provisioning/datasources/datasource.yaml b/provisioning/datasources/datasource.yaml index dff00fa..141967f 100644 --- a/provisioning/datasources/datasource.yaml +++ b/provisioning/datasources/datasource.yaml @@ -70,12 +70,23 @@ datasources: editable: true jsonData: feed: https://github.com/sonatype/nexus-public/releases.atom - - name: Volkov Labs YouTube + - name: Grafana Cloud RSS type: volkovlabs-rss-datasource access: proxy orgId: 1 - uid: rZAdZdf82 + uid: rss1 version: 1 editable: true jsonData: - feed: https://www.youtube.com/feeds/videos.xml?channel_id=UCQadniwbukI6ZmTN2oTTb-g + feed: https://grafana.com/docs/grafana-cloud/whats-new/index.xml + - name: Grafana Cloud Infinity + type: yesoreyeram-infinity-datasource + access: proxy + isDefault: false + orgId: 1 + uid: infinity1 + version: 1 + editable: true + jsonData: + url: https://grafana.com/docs/grafana-cloud/whats-new/index.xml + feedType: rss From 53e969dc03321b0333e9c1530ba9323d2e140177 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Fri, 24 May 2024 13:33:49 +0300 Subject: [PATCH 03/13] should added for test names --- src/utils.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 0381926..2581743 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -4,12 +4,12 @@ import { createUniqueKeyObject } from './utils'; * createUniqueKeyObject */ describe('createUniqueKeyObject', () => { - it('Returns an empty object for an empty input array', () => { + it('Should returns an empty object for an empty input array', () => { const result = createUniqueKeyObject([]); expect(result).toEqual({}); }); - it('Creates a unique key object for a non-empty array', () => { + it('Should creates a unique key object for a non-empty array', () => { const items = [ { name: 'John', age: 30 }, { name: 'Jane', email: 'jane@example.com' }, @@ -24,7 +24,7 @@ describe('createUniqueKeyObject', () => { }); }); - it('handles duplicate keys in the array', () => { + it('Should handles duplicate keys in the array', () => { const items = [ { name: 'John', age: 30 }, { name: 'Jane', age: 35 }, From 87fd1f9c2de48e762e0813bf3155fcb6d8176fd0 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Fri, 24 May 2024 13:38:42 +0300 Subject: [PATCH 04/13] test name updated --- src/utils.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 2581743..c96b772 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -4,12 +4,12 @@ import { createUniqueKeyObject } from './utils'; * createUniqueKeyObject */ describe('createUniqueKeyObject', () => { - it('Should returns an empty object for an empty input array', () => { + it('Should return an empty object for an empty input array', () => { const result = createUniqueKeyObject([]); expect(result).toEqual({}); }); - it('Should creates a unique key object for a non-empty array', () => { + it('Should create a unique key object for a non-empty array', () => { const items = [ { name: 'John', age: 30 }, { name: 'Jane', email: 'jane@example.com' }, @@ -24,7 +24,7 @@ describe('createUniqueKeyObject', () => { }); }); - it('Should handles duplicate keys in the array', () => { + it('Should handle duplicate keys in the array', () => { const items = [ { name: 'John', age: 30 }, { name: 'Jane', age: 35 }, From 15c6976b86ab4ac645afb72a388cf092f0c2c183 Mon Sep 17 00:00:00 2001 From: Mikhail Volkov Date: Sun, 26 May 2024 13:14:09 -0400 Subject: [PATCH 05/13] Return Data source --- provisioning/datasources/datasource.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/provisioning/datasources/datasource.yaml b/provisioning/datasources/datasource.yaml index 2ba2cf9..9610cef 100644 --- a/provisioning/datasources/datasource.yaml +++ b/provisioning/datasources/datasource.yaml @@ -79,6 +79,15 @@ datasources: editable: true jsonData: feed: https://grafana.com/docs/grafana-cloud/whats-new/index.xml + - name: Volkov Labs YouTube + type: volkovlabs-rss-datasource + access: proxy + orgId: 1 + uid: rZAdZdf82 + version: 1 + editable: true + jsonData: + feed: https://www.youtube.com/feeds/videos.xml?channel_id=UCQadniwbukI6ZmTN2oTTb-g - name: Grafana Cloud Infinity type: yesoreyeram-infinity-datasource access: proxy From 39abb8d22e2d4d2782d02dbf8eb6c1a0594ed2d4 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Mon, 27 May 2024 17:20:41 +0300 Subject: [PATCH 06/13] updated api.ts --- src/api/api.test.ts | 155 ++++++++++++++++++++++++++++++++++++++++++++ src/api/api.ts | 23 +++++-- 2 files changed, 171 insertions(+), 7 deletions(-) diff --git a/src/api/api.test.ts b/src/api/api.test.ts index b97392e..543e0a7 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -449,5 +449,160 @@ describe('Api', () => { expect(result?.length).toEqual(1); expect(tagField?.values).toEqual([null, 'RSS Tag 2']); }); + + /** + * Source with meta + */ + it('Should not return empty meta', async () => { + xmlResponse.data = ` + + + + + RSS Tutorial + + Test.

]]> + + + +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + + const metaField = result[0].fields.find((elem) => elem.name === 'meta'); + + expect(result?.length).toEqual(1); + expect(metaField?.values.length).toEqual(2); + expect(metaField?.values).toEqual([null, null]); + }); + + /** + * Source with meta + */ + it('Should not repeat a property in meta', async () => { + xmlResponse.data = ` + + + + + RSS Tutorial + + Test.

]]> + + + +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + const ogImageField = result[0].fields.find((elem) => elem.name === 'og:image'); + + expect(result?.length).toEqual(1); + expect(ogImageField?.values.length).toEqual(2); + expect(ogImageField?.values).toEqual(['content', 'content']); + }); + + /** + * YouTube + */ + it('Should not repeat a property in media:group', async () => { + xmlResponse.data = ` + + yt:channel:UCQadniwbukI6ZmTN2oTTb-g + UCQadniwbukI6ZmTN2oTTb-g + Volkov Labs + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-01-18T15:17:13+00:00 + + yt:video:PaiGygHI-dA + PaiGygHI-dA + UCQadniwbukI6ZmTN2oTTb-g + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-20T18:10:53+00:00 + 2022-06-20T19:26:02+00:00 + + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + ABOUT THIS VIDEO Grafa + + + + + + + + yt:video:RAxqS2hpWkg + RAxqS2hpWkg + UCQadniwbukI6ZmTN2oTTb-g + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-08T22:26:34+00:00 + 2022-06-09T00:10:05+00:00 + + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + ABOUT THIS VIDEO In the m + + + + + + +`; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + const thumbnail = result[0].fields.find((elem) => elem.name === 'media:group:media:thumbnail:url'); + const description = result[0].fields.find((elem) => elem.name === 'media:group:media:description'); + const content = result[0].fields.find((elem) => elem.name === 'media:group:media:content:url'); + + expect(thumbnail?.values.length).toEqual(2); + expect(description?.values.length).toEqual(2); + expect(content?.values.length).toEqual(2); + expect(thumbnail?.values).toEqual([ + 'https://i1.ytimg.com/vi/PaiGygHI-dA/hqdefault.jpg', + 'https://i3.ytimg.com/vi/RAxqS2hpWkg/hqdefault.jpg', + ]); + expect(description?.values).toEqual(['ABOUT THIS VIDEO Grafa', 'ABOUT THIS VIDEO Grafa In the m']); + expect(content?.values).toEqual([ + 'https://www.youtube.com/v/PaiGygHI-dA?version=3', + 'https://www.youtube.com/v/RAxqS2hpWkg?version=3', + ]); + }); }); }); diff --git a/src/api/api.ts b/src/api/api.ts index 8c405a0..4610c0a 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -120,11 +120,15 @@ export class Api { } /** - * Configure Items + * Configure Keys * Take all the unique keys in all items */ - const items: FeedItems = createUniqueKeyObject(channel.item); + const keys: FeedItems = createUniqueKeyObject(channel.item); + /** + * Configure Items + */ + const items: FeedItems = {}; /** * Find all items */ @@ -136,7 +140,7 @@ export class Api { return; } - Object.keys(items).forEach((key: string) => { + Object.keys(keys).forEach((key: string) => { let currentKey = key; let value = item[key]; @@ -152,8 +156,8 @@ export class Api { if (key === ItemKey.META && (value as Record)['@_property'] === MetaProperties.OG_IMAGE) { currentKey = MetaProperties.OG_IMAGE; value = (value as Record)['@_content']; + setItem(items, key, null); } - /** * Parse Guid */ @@ -241,10 +245,15 @@ export class Api { } /** - * Configure entries + * Configure entries Keys * Take all the unique keys in all entries */ - const entries: FeedItems = createUniqueKeyObject(feed.entry); + const entriesKeys: FeedItems = createUniqueKeyObject(feed.entry); + + /** + * Configure entries + */ + const entries: FeedItems = {}; /** * Find all entries @@ -257,7 +266,7 @@ export class Api { return; } - Object.keys(entries).forEach((key: string) => { + Object.keys(entriesKeys).forEach((key: string) => { let value = entry[key]; /** From f48702e0b9591f66eed882701c7027533dfb5549 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Mon, 27 May 2024 18:24:30 +0300 Subject: [PATCH 07/13] CI updated --- src/api/api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/api.test.ts b/src/api/api.test.ts index 543e0a7..2555f73 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -598,7 +598,7 @@ describe('Api', () => { 'https://i1.ytimg.com/vi/PaiGygHI-dA/hqdefault.jpg', 'https://i3.ytimg.com/vi/RAxqS2hpWkg/hqdefault.jpg', ]); - expect(description?.values).toEqual(['ABOUT THIS VIDEO Grafa', 'ABOUT THIS VIDEO Grafa In the m']); + expect(description?.values).toEqual(['ABOUT THIS VIDEO Grafa', 'ABOUT THIS VIDEO In the m']); expect(content?.values).toEqual([ 'https://www.youtube.com/v/PaiGygHI-dA?version=3', 'https://www.youtube.com/v/RAxqS2hpWkg?version=3', From 424ca22cc3df89527ad199fac815d11334fc46f0 Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Tue, 28 May 2024 16:15:39 +0300 Subject: [PATCH 08/13] updated News data; added new tests --- src/api/api.test.ts | 697 ++++++++++++++++++++++++++------------------ src/types.ts | 23 +- src/utils.test.ts | 84 +++++- src/utils.ts | 73 ++++- 4 files changed, 576 insertions(+), 301 deletions(-) diff --git a/src/api/api.test.ts b/src/api/api.test.ts index 2555f73..f8f02b9 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -134,34 +134,34 @@ describe('Api', () => { it('Should make getFeed request for Atom', async () => { xmlResponse.data = ` - - tag:status.redis.com,2005:/history - - - Redis Status - Incident History - 2021-12-26T15:23:56Z - - Redis - - - tag:status.redis.com,2005:Incident/8938651 - 2021-12-26T14:45:23Z - 2021-12-26T14:45:23Z - - Instance failure - <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> - <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> - - - tag:status.redis.com,2005:Incident/8899527 - 2021-12-21T01:37:14Z - 2021-12-21T01:37:14Z - - Scheduled Maintenance - <p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:37</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:01</var> UTC</small><br><strong>Monitoring</strong> - Service maintenance on AWS/us-west-2 applies to all resources with the following pattern in their endpoint : c2638.us-west-2-mz</p> - - `; + + tag:status.redis.com,2005:/history + + + Redis Status - Incident History + 2021-12-26T15:23:56Z + + Redis + + + tag:status.redis.com,2005:Incident/8938651 + 2021-12-26T14:45:23Z + 2021-12-26T14:45:23Z + + Instance failure + <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> + <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> + + + tag:status.redis.com,2005:Incident/8899527 + 2021-12-21T01:37:14Z + 2021-12-21T01:37:14Z + + Scheduled Maintenance + <p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:37</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:01</var> UTC</small><br><strong>Monitoring</strong> - Service maintenance on AWS/us-west-2 applies to all resources with the following pattern in their endpoint : c2638.us-west-2-mz</p> + + `; let result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ALL }, range); expect(result?.length).toEqual(2); @@ -175,34 +175,34 @@ describe('Api', () => { it('Should make getFeed request for Atom with Time Range', async () => { xmlResponse.data = ` - - tag:status.redis.com,2005:/history - - - Redis Status - Incident History - 2021-12-26T15:23:56Z - - Redis - - - tag:status.redis.com,2005:Incident/8938651 - 2021-12-26T14:45:23Z - 2021-12-26T14:45:23Z - - Instance failure - <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> - <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> - - - tag:status.redis.com,2005:Incident/8899527 - 2021-12-21T01:37:14Z - 2021-12-21T01:37:14Z - - Scheduled Maintenance - <p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:37</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:01</var> UTC</small><br><strong>Monitoring</strong> - Service maintenance on AWS/us-west-2 applies to all resources with the following pattern in their endpoint : c2638.us-west-2-mz</p> - - `; + + tag:status.redis.com,2005:/history + + + Redis Status - Incident History + 2021-12-26T15:23:56Z + + Redis + + + tag:status.redis.com,2005:Incident/8938651 + 2021-12-26T14:45:23Z + 2021-12-26T14:45:23Z + + Instance failure + <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> + <p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:45</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>26</var>, <var data-var=\'time\'>14:38</var> UTC</small><br><strong>Investigating</strong> - We are experiencing instance failure on GCP/us-east1 , this may affect resources that have the following pattern in their endpoint: c16307.us-east1-mz</p> + + + tag:status.redis.com,2005:Incident/8899527 + 2021-12-21T01:37:14Z + 2021-12-21T01:37:14Z + + Scheduled Maintenance + <p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:37</var> UTC</small><br><strong>Resolved</strong> - This incident has been resolved.</p><p><small>Dec <var data-var=\'date\'>21</var>, <var data-var=\'time\'>01:01</var> UTC</small><br><strong>Monitoring</strong> - Service maintenance on AWS/us-west-2 applies to all resources with the following pattern in their endpoint : c2638.us-west-2-mz</p> + + `; let result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ALL, dateField: 'updated' }, range2000); expect(result?.length).toEqual(2); @@ -234,11 +234,11 @@ describe('Api', () => { */ it('Should handle getFeed request for Atom with title only', async () => { xmlResponse.data = ` - - Test - - `; + xmlns:atom="http://www.w3.org/2005/Atom"> + + Test + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ALL }, range); expect(result?.length).toEqual(1); @@ -249,16 +249,16 @@ describe('Api', () => { */ it('Should handle getFeed request for Atom with image and no src', async () => { xmlResponse.data = ` - - - Test.
]]>
-
- -
]]> - - - `; + xmlns:atom="http://www.w3.org/2005/Atom"> + + + Test.
]]>
+
+ +
]]> + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); expect(result?.length).toEqual(1); @@ -269,24 +269,24 @@ describe('Api', () => { */ it('Should handle getFeed request for RSS 1.0', async () => { xmlResponse.data = ` - - - National Vulnerability Database - https://web.nvd.nist.gov/view/vuln/search - This feed contains the most recent CVE cyber vulnerabilities published within the National Vulnerability Database. - 2022-04-25T18:00:00Z - en-us - This material is not copywritten and may be freely used, however, attribution is requested. - - - CVE-2016-20014 - https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-20014 - In pam_tacplus.c in pam_tacplus before 1.4.1, pam_sm_acct_mgmt does not zero out the arep data structure. - 2022-04-21T04:15:09Z - - `; + + + National Vulnerability Database + https://web.nvd.nist.gov/view/vuln/search + This feed contains the most recent CVE cyber vulnerabilities published within the National Vulnerability Database. + 2022-04-25T18:00:00Z + en-us + This material is not copywritten and may be freely used, however, attribution is requested. + + + CVE-2016-20014 + https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-20014 + In pam_tacplus.c in pam_tacplus before 1.4.1, pam_sm_acct_mgmt does not zero out the arep data structure. + 2022-04-21T04:15:09Z + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); expect(result?.length).toEqual(1); @@ -298,24 +298,24 @@ describe('Api', () => { */ it('Should make getFeed request for GitHub Atom', async () => { xmlResponse.data = ` - - tag:github.com,2008:https://github.com/sonatype/nexus-public/releases - - - Release notes from nexus-public - 2022-03-29T22:38:42Z - - tag:github.com,2008:Repository/40029062/release-3.38.1-01 - 2022-03-30T09:02:42Z - - 3.38.1-01 - <p><a href="https://help.sonatype.com/repomanager3/product-information/release-notes/2022-release-notes/nexus-repository-3.38.0---3.38.1-release-notes" rel="nofollow">https://help.sonatype.com/repomanager3/product-information/release-notes/2022-release-notes/nexus-repository-3.38.0---3.38.1-release-notes</a></p> - - Blacktiger - - - - `; + + tag:github.com,2008:https://github.com/sonatype/nexus-public/releases + + + Release notes from nexus-public + 2022-03-29T22:38:42Z + + tag:github.com,2008:Repository/40029062/release-3.38.1-01 + 2022-03-30T09:02:42Z + + 3.38.1-01 + <p><a href="https://help.sonatype.com/repomanager3/product-information/release-notes/2022-release-notes/nexus-repository-3.38.0---3.38.1-release-notes" rel="nofollow">https://help.sonatype.com/repomanager3/product-information/release-notes/2022-release-notes/nexus-repository-3.38.0---3.38.1-release-notes</a></p> + + Blacktiger + + + + `; let result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ALL }, range); expect(result?.length).toEqual(2); @@ -352,65 +352,65 @@ describe('Api', () => { */ it('Should handle getFeed request for YouTube', async () => { xmlResponse.data = ` - - yt:channel:UCQadniwbukI6ZmTN2oTTb-g - UCQadniwbukI6ZmTN2oTTb-g - Volkov Labs - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-01-18T15:17:13+00:00 - - yt:video:PaiGygHI-dA - PaiGygHI-dA - UCQadniwbukI6ZmTN2oTTb-g - Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-06-20T18:10:53+00:00 - 2022-06-20T19:26:02+00:00 - - Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 - - - ABOUT THIS VIDEO Grafa - - - - - - - - yt:video:RAxqS2hpWkg - RAxqS2hpWkg - UCQadniwbukI6ZmTN2oTTb-g - RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-06-08T22:26:34+00:00 - 2022-06-09T00:10:05+00:00 - - RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard - - - ABOUT THIS VIDEO In the m - - - - - - -`; + xmlns:media="http://search.yahoo.com/mrss/" + xmlns="http://www.w3.org/2005/Atom"> + + yt:channel:UCQadniwbukI6ZmTN2oTTb-g + UCQadniwbukI6ZmTN2oTTb-g + Volkov Labs + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-01-18T15:17:13+00:00 + + yt:video:PaiGygHI-dA + PaiGygHI-dA + UCQadniwbukI6ZmTN2oTTb-g + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-20T18:10:53+00:00 + 2022-06-20T19:26:02+00:00 + + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + ABOUT THIS VIDEO Grafa + + + + + + + + yt:video:RAxqS2hpWkg + RAxqS2hpWkg + UCQadniwbukI6ZmTN2oTTb-g + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-08T22:26:34+00:00 + 2022-06-09T00:10:05+00:00 + + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + ABOUT THIS VIDEO In the m + + + + + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); expect(result?.length).toEqual(1); @@ -422,68 +422,67 @@ describe('Api', () => { */ it('Should return null value if key doesn`t contain in element', async () => { xmlResponse.data = ` - - - - RSS Tutorial - - Test.
]]>
-
- -
]]> - - RSS Tutorial 2 - - - RSS Tag 2 - - - - `; + xmlns:atom="http://www.w3.org/2005/Atom"> + + + + RSS Tutorial + + Test.
]]>
+
+ +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); const tagField = result[0].fields.find((elem) => elem.name === 'tag'); expect(result?.length).toEqual(1); - expect(tagField?.values).toEqual([null, 'RSS Tag 2']); + expect(tagField?.values).toEqual([undefined, 'RSS Tag 2']); }); /** * Source with meta */ - it('Should not return empty meta', async () => { + it('Should not return meta', async () => { xmlResponse.data = ` - - - - - RSS Tutorial - - Test.
]]>
-
- - -
]]> - - RSS Tutorial 2 - - - RSS Tag 2 - - - - `; + xmlns:atom="http://www.w3.org/2005/Atom"> + + + + + RSS Tutorial + + Test.
]]>
+
+ + +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); const metaField = result[0].fields.find((elem) => elem.name === 'meta'); expect(result?.length).toEqual(1); - expect(metaField?.values.length).toEqual(2); - expect(metaField?.values).toEqual([null, null]); + expect(metaField).toBe(undefined); }); /** @@ -491,27 +490,27 @@ describe('Api', () => { */ it('Should not repeat a property in meta', async () => { xmlResponse.data = ` - - - - - RSS Tutorial - - Test.
]]>
-
- - -
]]> - - RSS Tutorial 2 - - - RSS Tag 2 - - - - `; + xmlns:atom="http://www.w3.org/2005/Atom"> + + + + + RSS Tutorial + + Test.
]]>
+
+ + +
]]> + + RSS Tutorial 2 + + + RSS Tag 2 + + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); const ogImageField = result[0].fields.find((elem) => elem.name === 'og:image'); @@ -521,70 +520,188 @@ describe('Api', () => { expect(ogImageField?.values).toEqual(['content', 'content']); }); + /** + * Source with difference meta property + */ + it('Should return correct properties for meta tag', async () => { + xmlResponse.data = ` + + W3Schools Home Page + https://www.w3schools.com + Free web building tutorials + + + RSS Tutorial + https://www.w3schools.com/xml/xml_rss.asp + New RSS tutorial on W3Schools + + + + XML Tutorial 1 + https://www.w3schools.com/xml + New XML tutorial on W3Schools + + + + XML Tutorial 4 + https://www.w3schools.com/xml + New XML tutorial on W3Schools + + + + `; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + + const images = result[0].fields.find((elem) => elem.name === 'og:image'); + const titles = result[0].fields.find((elem) => elem.name === 'og:title'); + + expect(result?.length).toEqual(1); + + /** + * Return data Equalwith items length + */ + expect(images?.values.length).toEqual(3); + expect(titles?.values.length).toEqual(3); + + expect(images?.values).toEqual(['image', 'image', null]); + expect(titles?.values).toEqual([null, null, 'title']); + }); + + /** + * Source with content:encoded + */ + it('Should return correct properties for content:encoded tags', async () => { + xmlResponse.data = ` + + W3Schools Home Page + https://www.w3schools.com + Free web building tutorials + + How product teams can manage their performance using Grafana, Prometheus, and Oracle metrics + + https://grafana.com/blog/2021/12/23/how-product-teams-can-manage-their-performance-using-grafana-prometheus-and-oracle-metrics/?utm_source=grafana_news&utm_medium=rss + Thu, 23 Dec 2021 00:00:00 +0000 + https://grafana.com/blog/2021/12/23/how-product-teams-can-manage-their-performance-using-grafana-prometheus-and-oracle-metrics/?utm_source=grafana_news&utm_medium=rss&src=in-prod&plcmt=rss + Ever known a project manager who thinks a task takes minutes when it really takes hours? One company has developed a helpful monitoring tool that not only helps project managers make more realistic estimates, but also helps product teams save time, increase efficiency, and improve their overall performance.\nAt ObservabilityCON 2020, Walter Ritzel Paixão Côrtes, a product designer at Dell, gave a presentation about a data-driven solution his team developed called Product Team Observability. + + + h4 Text

When we created a Base64 image/PDF panel to display images for one of our projects, support for PDF documents was added as a feature. I am glad the panel is being used to display PDF documents stored in the databases like PostgreSQL.

]]>
+ https://volkovlabs.com/using-grafana-to-display-large-pdf-documents-weve-got-you-covered-4e654e8d4bce?source=rss----97b04264832a---4 +
+ + h4 Second text

When we created a Base64 image/PDF panel to display images for one of our projects, support for PDF documents was added as a feature. I am glad the panel is being used to display PDF documents stored in the databases like PostgreSQL.

]]>
+ https://volkovlabs.com/using-grafana-to-display-large-pdf-documents-weve-got-you-covered-4e654e8d4bce?source=rss----97b04264832a---4 +
+ + 123 +
]]> + + + https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png + Volkov Labs - Medium + https://volkovlabs.com?source=rss----97b04264832a---4 + + + + `; + + const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); + + const encodedBase = result[0].fields.find((elem) => elem.name === 'content:encoded'); + const encodedH4 = result[0].fields.find((elem) => elem.name === 'content:h4'); + const encodedImg = result[0].fields.find((elem) => elem.name === 'content:img'); + const encodedImgSrc = result[0].fields.find((elem) => elem.name === 'content:img-src'); + + expect(result?.length).toEqual(1); + + /** + * Return data Equalwith items length + */ + expect(encodedBase?.values.length).toEqual(4); + expect(encodedH4?.values.length).toEqual(4); + expect(encodedImg?.values.length).toEqual(4); + expect(encodedImgSrc?.values.length).toEqual(4); + + expect(encodedH4?.values).toEqual([undefined, 'h4 Text', 'h4 Second text', '']); + expect(encodedImg?.values).toEqual([ + undefined, + ``, + '', + '', + ]); + expect(encodedImgSrc?.values).toEqual([ + undefined, + `https://cdn-images-1.medium.com/max/1024/0*ZhXFfccuxa1PugIM`, + '', + '', + ]); + }); + /** * YouTube */ it('Should not repeat a property in media:group', async () => { xmlResponse.data = ` - - yt:channel:UCQadniwbukI6ZmTN2oTTb-g - UCQadniwbukI6ZmTN2oTTb-g - Volkov Labs - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-01-18T15:17:13+00:00 - - yt:video:PaiGygHI-dA - PaiGygHI-dA - UCQadniwbukI6ZmTN2oTTb-g - Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-06-20T18:10:53+00:00 - 2022-06-20T19:26:02+00:00 - - Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 - - - ABOUT THIS VIDEO Grafa - - - - - - - - yt:video:RAxqS2hpWkg - RAxqS2hpWkg - UCQadniwbukI6ZmTN2oTTb-g - RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard - - - Volkov Labs - https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g - - 2022-06-08T22:26:34+00:00 - 2022-06-09T00:10:05+00:00 - - RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard - - - ABOUT THIS VIDEO In the m - - - - - - -`; + xmlns:media="http://search.yahoo.com/mrss/" + xmlns="http://www.w3.org/2005/Atom"> + + yt:channel:UCQadniwbukI6ZmTN2oTTb-g + UCQadniwbukI6ZmTN2oTTb-g + Volkov Labs + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-01-18T15:17:13+00:00 + + yt:video:PaiGygHI-dA + PaiGygHI-dA + UCQadniwbukI6ZmTN2oTTb-g + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-20T18:10:53+00:00 + 2022-06-20T19:26:02+00:00 + + Favorite sessions of Grafana Conference 2022 | GrafanaCONline 2022 | Grafana 9 + + + ABOUT THIS VIDEO Grafa + + + + + + + + yt:video:RAxqS2hpWkg + RAxqS2hpWkg + UCQadniwbukI6ZmTN2oTTb-g + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + Volkov Labs + https://www.youtube.com/channel/UCQadniwbukI6ZmTN2oTTb-g + + 2022-06-08T22:26:34+00:00 + 2022-06-09T00:10:05+00:00 + + RSS/Atom Data Source for Grafana | News feed tutorial for Grafana Dashboard + + + ABOUT THIS VIDEO In the m + + + + + + + `; const result = await api.getFeed({ refId: 'A', feedType: FeedTypeValue.ITEMS }, range); const thumbnail = result[0].fields.find((elem) => elem.name === 'media:group:media:thumbnail:url'); diff --git a/src/types.ts b/src/types.ts index efd221a..bb6b1b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,7 +58,26 @@ export interface DataItem { /** * Data Item * - * @type {[key: string]: string | Record} + * @type {[key: string]: string | Record } */ - [key: string]: string | Record; + [key: string]: string | Record | Record; +} + +/** + * Unique Key Info + */ +export interface UniqueKeyInfo { + /** + * key Accessor + * + * @type {string} + */ + keyAccessor?: string; + + /** + * value Accessor + * + * @type {string} + */ + valueAccessor: string; } diff --git a/src/utils.test.ts b/src/utils.test.ts index c96b772..4146e1c 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,11 +1,11 @@ -import { createUniqueKeyObject } from './utils'; +import { getUniqueAtomKeys, getUniqueChannelKeys } from './utils'; /** - * createUniqueKeyObject + * getUniqueAtomKeys */ -describe('createUniqueKeyObject', () => { +describe('getUniqueAtomKeys', () => { it('Should return an empty object for an empty input array', () => { - const result = createUniqueKeyObject([]); + const result = getUniqueAtomKeys([]); expect(result).toEqual({}); }); @@ -16,7 +16,7 @@ describe('createUniqueKeyObject', () => { { name: 'Bob', age: 25, email: 'bob@example.com' }, ]; - const result = createUniqueKeyObject(items as any); + const result = getUniqueAtomKeys(items as any); expect(result).toEqual({ name: [], age: [], @@ -31,7 +31,7 @@ describe('createUniqueKeyObject', () => { { name: 'John', email: 'john@example.com' }, ]; - const result = createUniqueKeyObject(items as any); + const result = getUniqueAtomKeys(items as any); expect(result).toEqual({ name: [], age: [], @@ -39,3 +39,75 @@ describe('createUniqueKeyObject', () => { }); }); }); + +/** + * getUniqueChannelKeys + */ +describe('getUniqueChannelKeys', () => { + it('Should return an empty object for an empty input array', () => { + const result = getUniqueChannelKeys([]); + expect(result).toEqual({}); + }); + + it('should return the correct unique channel keys', () => { + const items = [ + { + title: 'Item 1', + description: 'Description 1', + guid: { + '#text': 'guid-1', + }, + meta: { + '@_property': 'og:title', + '@_content': 'Title', + }, + }, + { + title: 'Item 2', + description: 'Description 2', + 'content:encoded': '

Content 1

', + guid: { + '#text': 'guid-2', + }, + meta: { + '@_property': 'og:image', + '@_content': 'src image', + }, + }, + ] as any; + + const expectedResult = { + title: { + valueAccessor: 'title', + }, + description: { + valueAccessor: 'description', + }, + guid: { + valueAccessor: 'guid.#text', + }, + 'og:image': { + keyAccessor: 'meta.@_property', + valueAccessor: 'meta.@_content', + }, + 'og:title': { + keyAccessor: 'meta.@_property', + valueAccessor: 'meta.@_content', + }, + 'content:encoded': { + valueAccessor: 'content:encoded', + }, + 'content:h4': { + valueAccessor: 'content:encoded', + }, + 'content:img': { + valueAccessor: 'content:encoded', + }, + 'content:img-src': { + valueAccessor: 'content:encoded', + }, + }; + + expect(getUniqueChannelKeys(items)).toEqual(expectedResult); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 1c2471d..7edd726 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ import { TimeRange } from '@grafana/data'; -import { DataItem, FeedItems } from 'types'; +import { get } from 'lodash'; +import { DataItem, FeedItems, UniqueKeyInfo } from 'types'; + +import { ItemKey } from './constants'; /** * Set or added Item values */ @@ -32,8 +35,8 @@ export const isDateBetweenRange = (value: string, range: TimeRange): boolean => * Сreate Unique Key Object * Linear pass of items and forming an object with unique keys based on an array of objects */ -export const createUniqueKeyObject = (items: DataItem[]) => - items.reduce((result: Record, obj) => { +export const getUniqueAtomKeys = (items: DataItem[]) => { + return items.reduce((result: Record, obj) => { Object.keys(obj).forEach((key) => { if (!result[key]) { result[key] = []; @@ -41,3 +44,67 @@ export const createUniqueKeyObject = (items: DataItem[]) => }); return result; }, {}); +}; + +export const getUniqueChannelKeys = (items: DataItem[]) => { + const result: Record = {}; + + /** + * Go through all the objects in the array + */ + for (const item of items) { + /** + * Handle other keys + */ + for (const key in item) { + if (key !== ItemKey.META && key !== ItemKey.GUID && !result[key]) { + result[key] = { + valueAccessor: key, + }; + } + + /** + * If the object has a guid key + */ + if (key === ItemKey.GUID) { + result[key] = { + valueAccessor: `guid.#text`, + }; + } + + /** + * If the object has a content:encoded key + */ + if (key === ItemKey.CONTENT_ENCODED) { + const contentEncodedKeys = [ItemKey.CONTENT_H4, ItemKey.CONTENT_IMG, ItemKey.CONTENT_IMG_SRC]; + + contentEncodedKeys.forEach((contentKey) => { + if (!result[contentKey]) { + result[contentKey] = { + valueAccessor: key, + }; + } + }); + } + + /** + * If the object has a “meta” key + */ + if (key === ItemKey.META) { + const property = get(item.meta, '@_property'); + + /** + * If the “meta” key has not yet been added to the resulting object + */ + if (property && !result[property]) { + result[property] = { + keyAccessor: `meta.@_property`, + valueAccessor: `meta.@_content`, + }; + } + } + } + } + + return result; +}; From e23310210f45af651863bdb2f313beebed26243a Mon Sep 17 00:00:00 2001 From: vitPinchuk Date: Tue, 28 May 2024 16:16:09 +0300 Subject: [PATCH 09/13] api.tts file refactoring --- src/api/api.ts | 71 ++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index 4610c0a..9930e7f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,11 +1,12 @@ import { createDataFrame, DataFrame, DataSourceInstanceSettings, FieldType, TimeRange } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { XMLParser } from 'fast-xml-parser'; +import { get } from 'lodash'; import { lastValueFrom } from 'rxjs'; -import { ALWAYS_ARRAY, FeedTypeValue, ItemKey, MetaProperties } from '../constants'; +import { ALWAYS_ARRAY, FeedTypeValue, ItemKey } from '../constants'; import { DataItem, DataSourceOptions, FeedItems, Query } from '../types'; -import { createUniqueKeyObject, isDateBetweenRange, setItem } from '../utils'; +import { getUniqueAtomKeys, getUniqueChannelKeys, isDateBetweenRange, setItem } from '../utils'; /** * API @@ -123,7 +124,7 @@ export class Api { * Configure Keys * Take all the unique keys in all items */ - const keys: FeedItems = createUniqueKeyObject(channel.item); + const channelKeys = getUniqueChannelKeys(channel.item); /** * Configure Items @@ -140,50 +141,52 @@ export class Api { return; } - Object.keys(keys).forEach((key: string) => { - let currentKey = key; - let value = item[key]; + Object.keys(channelKeys).forEach((key) => { + const keyObject = channelKeys[key]; /** - * If Item doesn`t contain key set key with null value to avoid mixed up + * Check key with Accessor (keys for meta tag) */ - if (!value) { - setItem(items, currentKey, null); - } else { - /** - * Parse Meta - */ - if (key === ItemKey.META && (value as Record)['@_property'] === MetaProperties.OG_IMAGE) { - currentKey = MetaProperties.OG_IMAGE; - value = (value as Record)['@_content']; - setItem(items, key, null); - } - /** - * Parse Guid - */ - if (key === ItemKey.GUID && (value as Record)['#text']) { - value = (value as Record)['#text']; - } + if (!keyObject.keyAccessor) { + let value = get(item, keyObject.valueAccessor); /** * Parse Encoded content for H4 and first Image */ - if (key === ItemKey.CONTENT_ENCODED) { + if (key === ItemKey.CONTENT_H4 && value) { const h4 = value.toString().match(/

(.*?)<\/h4>/); + value = h4?.length ? h4[1] : ''; + } + + if (key === ItemKey.CONTENT_IMG && value) { const figure = value.toString().match(/
(.*?)<\/figure>/); + value = figure?.length ? figure[1] : ''; + } - setItem(items, ItemKey.CONTENT_H4, h4?.length ? h4[1] : ''); + if (key === ItemKey.CONTENT_IMG_SRC && value) { + const figure = value.toString().match(/
(.*?)<\/figure>/); + const img = figure?.length ? figure[1].match(//) : null; + value = img?.length ? img[1] : ''; + } + + setItem(items, key, value as string); + } else { + /** + * Get key for item + */ + const fieldKey = get(item, keyObject.keyAccessor); + if (key === fieldKey) { /** - * Extract image and source + * Set value for key */ - if (figure?.length) { - setItem(items, ItemKey.CONTENT_IMG, figure[1]); - const img = figure[1].match(//); - setItem(items, ItemKey.CONTENT_IMG_SRC, img?.length ? img[1] : ''); - } + setItem(items, key, get(item, keyObject.valueAccessor) as string); + } else { + /** + * Set null + */ + setItem(items, key, null); } - setItem(items, currentKey, value as string); } }); }); @@ -248,7 +251,7 @@ export class Api { * Configure entries Keys * Take all the unique keys in all entries */ - const entriesKeys: FeedItems = createUniqueKeyObject(feed.entry); + const entriesKeys: FeedItems = getUniqueAtomKeys(feed.entry); /** * Configure entries From 8e2ef0405a006c39be2ecdab8e752219313e51ec Mon Sep 17 00:00:00 2001 From: asimonok Date: Wed, 29 May 2024 16:30:17 +0300 Subject: [PATCH 10/13] Formatting --- src/api/api.ts | 40 +++++----- src/types.ts | 8 +- src/utils.test.ts | 186 +++++++++++++++++++++++----------------------- src/utils.ts | 18 +++-- 4 files changed, 127 insertions(+), 125 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index 9930e7f..7538750 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -6,7 +6,7 @@ import { lastValueFrom } from 'rxjs'; import { ALWAYS_ARRAY, FeedTypeValue, ItemKey } from '../constants'; import { DataItem, DataSourceOptions, FeedItems, Query } from '../types'; -import { getUniqueAtomKeys, getUniqueChannelKeys, isDateBetweenRange, setItem } from '../utils'; +import { getUniqueAtomKeys, getAllItemKeyConfigs, isDateBetweenRange, setItem } from '../utils'; /** * API @@ -124,12 +124,13 @@ export class Api { * Configure Keys * Take all the unique keys in all items */ - const channelKeys = getUniqueChannelKeys(channel.item); + const channelKeys = getAllItemKeyConfigs(channel.item); /** * Configure Items */ const items: FeedItems = {}; + /** * Find all items */ @@ -142,13 +143,13 @@ export class Api { } Object.keys(channelKeys).forEach((key) => { - const keyObject = channelKeys[key]; + const keyConfig = channelKeys[key]; /** * Check key with Accessor (keys for meta tag) */ - if (!keyObject.keyAccessor) { - let value = get(item, keyObject.valueAccessor); + if (!keyConfig.keyAccessor) { + let value = get(item, keyConfig.valueAccessor); /** * Parse Encoded content for H4 and first Image @@ -170,23 +171,24 @@ export class Api { } setItem(items, key, value as string); + return; + } + + /** + * Get key for item + */ + const itemKey = get(item, keyConfig.keyAccessor); + + if (key === itemKey) { + /** + * Set value for key + */ + setItem(items, key, get(item, keyConfig.valueAccessor) as string); } else { /** - * Get key for item + * Set null */ - - const fieldKey = get(item, keyObject.keyAccessor); - if (key === fieldKey) { - /** - * Set value for key - */ - setItem(items, key, get(item, keyObject.valueAccessor) as string); - } else { - /** - * Set null - */ - setItem(items, key, null); - } + setItem(items, key, null); } }); }); diff --git a/src/types.ts b/src/types.ts index bb6b1b0..71e8b8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,18 +64,18 @@ export interface DataItem { } /** - * Unique Key Info + * Key Config */ -export interface UniqueKeyInfo { +export interface KeyConfig { /** - * key Accessor + * Key Accessor * * @type {string} */ keyAccessor?: string; /** - * value Accessor + * Value Accessor * * @type {string} */ diff --git a/src/utils.test.ts b/src/utils.test.ts index 4146e1c..44b3431 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,113 +1,109 @@ -import { getUniqueAtomKeys, getUniqueChannelKeys } from './utils'; +import { getUniqueAtomKeys, getAllItemKeyConfigs } from './utils'; -/** - * getUniqueAtomKeys - */ -describe('getUniqueAtomKeys', () => { - it('Should return an empty object for an empty input array', () => { - const result = getUniqueAtomKeys([]); - expect(result).toEqual({}); - }); +describe('utils', () => { + describe('getUniqueAtomKeys', () => { + it('Should return an empty object for an empty input array', () => { + const result = getUniqueAtomKeys([]); + expect(result).toEqual({}); + }); - it('Should create a unique key object for a non-empty array', () => { - const items = [ - { name: 'John', age: 30 }, - { name: 'Jane', email: 'jane@example.com' }, - { name: 'Bob', age: 25, email: 'bob@example.com' }, - ]; + it('Should create a unique key object for a non-empty array', () => { + const items = [ + { name: 'John', age: 30 }, + { name: 'Jane', email: 'jane@example.com' }, + { name: 'Bob', age: 25, email: 'bob@example.com' }, + ]; - const result = getUniqueAtomKeys(items as any); - expect(result).toEqual({ - name: [], - age: [], - email: [], + const result = getUniqueAtomKeys(items as any); + expect(result).toEqual({ + name: [], + age: [], + email: [], + }); }); - }); - it('Should handle duplicate keys in the array', () => { - const items = [ - { name: 'John', age: 30 }, - { name: 'Jane', age: 35 }, - { name: 'John', email: 'john@example.com' }, - ]; + it('Should handle duplicate keys in the array', () => { + const items = [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 35 }, + { name: 'John', email: 'john@example.com' }, + ]; - const result = getUniqueAtomKeys(items as any); - expect(result).toEqual({ - name: [], - age: [], - email: [], + const result = getUniqueAtomKeys(items as any); + expect(result).toEqual({ + name: [], + age: [], + email: [], + }); }); }); -}); -/** - * getUniqueChannelKeys - */ -describe('getUniqueChannelKeys', () => { - it('Should return an empty object for an empty input array', () => { - const result = getUniqueChannelKeys([]); - expect(result).toEqual({}); - }); + describe('getAllItemKeyConfigs', () => { + it('Should return an empty object for an empty input array', () => { + const result = getAllItemKeyConfigs([]); + expect(result).toEqual({}); + }); - it('should return the correct unique channel keys', () => { - const items = [ - { - title: 'Item 1', - description: 'Description 1', - guid: { - '#text': 'guid-1', + it('should return the correct unique channel keys', () => { + const items = [ + { + title: 'Item 1', + description: 'Description 1', + guid: { + '#text': 'guid-1', + }, + meta: { + '@_property': 'og:title', + '@_content': 'Title', + }, }, - meta: { - '@_property': 'og:title', - '@_content': 'Title', + { + title: 'Item 2', + description: 'Description 2', + 'content:encoded': '

Content 1

', + guid: { + '#text': 'guid-2', + }, + meta: { + '@_property': 'og:image', + '@_content': 'src image', + }, + }, + ] as any; + + const expectedResult = { + title: { + valueAccessor: 'title', + }, + description: { + valueAccessor: 'description', }, - }, - { - title: 'Item 2', - description: 'Description 2', - 'content:encoded': '

Content 1

', guid: { - '#text': 'guid-2', + valueAccessor: 'guid.#text', }, - meta: { - '@_property': 'og:image', - '@_content': 'src image', + 'og:image': { + keyAccessor: 'meta.@_property', + valueAccessor: 'meta.@_content', }, - }, - ] as any; - - const expectedResult = { - title: { - valueAccessor: 'title', - }, - description: { - valueAccessor: 'description', - }, - guid: { - valueAccessor: 'guid.#text', - }, - 'og:image': { - keyAccessor: 'meta.@_property', - valueAccessor: 'meta.@_content', - }, - 'og:title': { - keyAccessor: 'meta.@_property', - valueAccessor: 'meta.@_content', - }, - 'content:encoded': { - valueAccessor: 'content:encoded', - }, - 'content:h4': { - valueAccessor: 'content:encoded', - }, - 'content:img': { - valueAccessor: 'content:encoded', - }, - 'content:img-src': { - valueAccessor: 'content:encoded', - }, - }; + 'og:title': { + keyAccessor: 'meta.@_property', + valueAccessor: 'meta.@_content', + }, + 'content:encoded': { + valueAccessor: 'content:encoded', + }, + 'content:h4': { + valueAccessor: 'content:encoded', + }, + 'content:img': { + valueAccessor: 'content:encoded', + }, + 'content:img-src': { + valueAccessor: 'content:encoded', + }, + }; - expect(getUniqueChannelKeys(items)).toEqual(expectedResult); + expect(getAllItemKeyConfigs(items)).toEqual(expectedResult); + }); }); }); diff --git a/src/utils.ts b/src/utils.ts index 7edd726..0d9af60 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { TimeRange } from '@grafana/data'; import { get } from 'lodash'; -import { DataItem, FeedItems, UniqueKeyInfo } from 'types'; +import { DataItem, FeedItems, KeyConfig } from 'types'; import { ItemKey } from './constants'; /** @@ -32,7 +32,7 @@ export const isDateBetweenRange = (value: string, range: TimeRange): boolean => }; /** - * Сreate Unique Key Object + * Create Unique Key Object * Linear pass of items and forming an object with unique keys based on an array of objects */ export const getUniqueAtomKeys = (items: DataItem[]) => { @@ -46,8 +46,12 @@ export const getUniqueAtomKeys = (items: DataItem[]) => { }, {}); }; -export const getUniqueChannelKeys = (items: DataItem[]) => { - const result: Record = {}; +/** + * Get all item key configs + * @param items + */ +export const getAllItemKeyConfigs = (items: DataItem[]): Record => { + const result: Record = {}; /** * Go through all the objects in the array @@ -68,7 +72,7 @@ export const getUniqueChannelKeys = (items: DataItem[]) => { */ if (key === ItemKey.GUID) { result[key] = { - valueAccessor: `guid.#text`, + valueAccessor: 'guid.#text', }; } @@ -98,8 +102,8 @@ export const getUniqueChannelKeys = (items: DataItem[]) => { */ if (property && !result[property]) { result[property] = { - keyAccessor: `meta.@_property`, - valueAccessor: `meta.@_content`, + keyAccessor: 'meta.@_property', + valueAccessor: 'meta.@_content', }; } } From 9c916a50d43f035e88833e30bcfa88190aba693b Mon Sep 17 00:00:00 2001 From: asimonok Date: Wed, 29 May 2024 16:32:56 +0300 Subject: [PATCH 11/13] Fix lint errors --- src/api/api.ts | 2 +- src/utils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index 7538750..727bd8a 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -6,7 +6,7 @@ import { lastValueFrom } from 'rxjs'; import { ALWAYS_ARRAY, FeedTypeValue, ItemKey } from '../constants'; import { DataItem, DataSourceOptions, FeedItems, Query } from '../types'; -import { getUniqueAtomKeys, getAllItemKeyConfigs, isDateBetweenRange, setItem } from '../utils'; +import { getAllItemKeyConfigs, getUniqueAtomKeys, isDateBetweenRange, setItem } from '../utils'; /** * API diff --git a/src/utils.test.ts b/src/utils.test.ts index 44b3431..c5e2a0e 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,4 +1,4 @@ -import { getUniqueAtomKeys, getAllItemKeyConfigs } from './utils'; +import { getAllItemKeyConfigs, getUniqueAtomKeys } from './utils'; describe('utils', () => { describe('getUniqueAtomKeys', () => { From 721f544f2cc00315b411547bbe955fd253605eb9 Mon Sep 17 00:00:00 2001 From: Mikhail Volkov Date: Fri, 7 Jun 2024 00:04:29 -0400 Subject: [PATCH 12/13] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a7500..2c530d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Change Log -## 4.1.0 (IN PROGRESS) +## 4.1.0 (2024-06-07) ### Features / Enhancements - Added tooltip to Feed URL (#73) - Added XML Server (#75) - Updated to Grafana 11.0 and dependencies (#72) +- Update fields in items getting mixed up (#76) ## 4.0.0 (2024-05-09) From 38fbd81468627f17fbe0e7f9efb6112d4f9f7815 Mon Sep 17 00:00:00 2001 From: Mikhail Volkov Date: Fri, 7 Jun 2024 00:05:28 -0400 Subject: [PATCH 13/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 490e0bf..4fc30db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Dashboard](https://raw.githubusercontent.com/VolkovLabs/volkovlabs-rss-datasource/main/src/img/dashboard.png) -![Grafana](https://img.shields.io/badge/Grafana-11-orange) +![Grafana](https://img.shields.io/badge/Grafana-11.0-orange) ![CI](https://github.com/volkovlabs/volkovlabs-rss-datasource/workflows/CI/badge.svg) ![E2E](https://github.com/volkovlabs/volkovlabs-rss-datasource/workflows/E2E/badge.svg) [![codecov](https://codecov.io/gh/VolkovLabs/volkovlabs-rss-datasource/branch/main/graph/badge.svg?token=2W9VR0PG5N)](https://codecov.io/gh/VolkovLabs/volkovlabs-rss-datasource)