diff --git a/package-lock.json b/package-lock.json index 4a10229ea..7aeca1bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.0-patch-1-beta-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.0-patch-1-beta-3", "license": "ISC", "dependencies": { "@types/react-dates": "^21.8.6", @@ -26,6 +26,7 @@ "@testing-library/react": "^12.1.4", "@tippyjs/react": "^4.2.0", "@typeform/embed-react": "2.20.0", + "@types/dompurify": "^3.0.5", "@types/react": "17.0.39", "@types/react-dom": "17.0.13", "@types/react-router-dom": "^5.3.3", @@ -2969,6 +2970,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3159,6 +3169,12 @@ "@types/jest": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", diff --git a/package.json b/package.json index 5678e1dad..f4c42dd5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.0-patch-1-beta-3", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -40,6 +40,7 @@ "@testing-library/react": "^12.1.4", "@tippyjs/react": "^4.2.0", "@typeform/embed-react": "2.20.0", + "@types/dompurify": "^3.0.5", "@types/react": "17.0.39", "@types/react-dom": "17.0.13", "@types/react-router-dom": "^5.3.3", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index f15e80ef6..7eb32cda9 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -66,6 +66,7 @@ export const URLS = { GLOBAL_CONFIG_SCOPED_VARIABLES: '/global-config/scoped-variables', GLOBAL_CONFIG_DEPLOYMENT_CHARTS_LIST: '/global-config/deployment-charts', NETWORK_STATUS_INTERFACE: '/network-status-interface', + RESOURCE_BROWSER: '/resource-browser', } export const ROUTES = { diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index 2dc8692ea..17436924d 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -15,6 +15,7 @@ */ import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import DOMPurify from 'dompurify' import { JSONPath, JSONPathOptions } from 'jsonpath-plus' import { compare as compareJSON, applyPatch } from 'fast-json-patch' import { components } from 'react-select' @@ -642,7 +643,11 @@ export const powerSetOfSubstringsFromStart = (strings: string[], regex: RegExp) return _keys }) -export const convertJSONPointerToJSONPath = (pointer: string) => pointer.replace(/\/([\*0-9]+)\//g, '[$1].').replace(/\//g, '.').replace(/\./, '$.') +export const convertJSONPointerToJSONPath = (pointer: string) => + pointer + .replace(/\/([\*0-9]+)\//g, '[$1].') + .replace(/\//g, '.') + .replace(/\./, '$.') export const flatMapOfJSONPaths = ( paths: string[], @@ -953,3 +958,42 @@ export const throttle = unknown>( } } } + +// TODO: Might need to expose sandbox and referrer policy +export const getSanitizedIframe = (iframeString: string) => + DOMPurify.sanitize(iframeString, { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'], + }) + +/** + * This method adds default attributes to iframe - title, loading ="lazy", width="100%", height="100%" + */ +export const getIframeWithDefaultAttributes = (iframeString: string, defaultName?: string): string => { + const parentDiv = document.createElement('div') + parentDiv.innerHTML = getSanitizedIframe(iframeString) + + + const iframe = parentDiv.querySelector('iframe') + if (iframe) { + if (!iframe.hasAttribute('title') && !!defaultName) { + iframe.setAttribute('title', defaultName) + } + + if (!iframe.hasAttribute('loading')) { + iframe.setAttribute('loading', 'lazy') + } + + if (!iframe.hasAttribute('width')) { + iframe.setAttribute('width', '100%') + } + + if (!iframe.hasAttribute('height')) { + iframe.setAttribute('height', '100%') + } + + return parentDiv.innerHTML + } + + return iframeString +} diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 7ec58c71f..41f51cf79 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Dayjs } from 'dayjs' import { OptionType, CommonNodeAttr, @@ -118,6 +119,7 @@ export enum Nodes { Event = 'Event', Namespace = 'Namespace', Overview = 'Overview', + MonitoringDashboard = 'MonitoringDashboard', } // FIXME: This should be `typeof Nodes[keyof typeof Nodes]` instead since the key and values are not the same. Same to be removed from duplications in dashboard @@ -680,6 +682,7 @@ export enum ConfigurationType { export interface BaseURLParams { appId: string envId: string + clusterId: string } export interface ConfigKeysWithLockType { @@ -738,3 +741,31 @@ export interface CustomRoleAndMeta { possibleRolesMetaForCluster: MetaPossibleRoles possibleRolesMetaForJob: MetaPossibleRoles } + +interface CommonTabArgsType { + name: string + kind?: string + url: string + isSelected: boolean + title?: string + isDeleted?: boolean + position: number + iconPath?: string + dynamicTitle?: string + showNameOnSelect?: boolean + /** + * @default false + */ + hideName?: boolean + isAlive?: boolean + lastSyncMoment?: Dayjs + componentKey?: string +} + +export interface InitTabType extends CommonTabArgsType { + idPrefix: string +} + +export interface DynamicTabType extends CommonTabArgsType { + id: string +} diff --git a/src/Shared/validations.tsx b/src/Shared/validations.tsx index 4a8454c5f..81fdd4ab7 100644 --- a/src/Shared/validations.tsx +++ b/src/Shared/validations.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { getSanitizedIframe } from '@Common/Helper' import { URLProtocolType } from './types' export interface ValidationResponseType { @@ -342,3 +343,29 @@ export const validateJSON = (json: string): ValidationResponseType => { } } } + +export const validateIframe = (input: string): ValidationResponseType => { + const sanitizedInput = getSanitizedIframe(input) + const parentDiv = document.createElement('div') + parentDiv.innerHTML = sanitizedInput + + const iframe = parentDiv.querySelector('iframe') + + // TODO: Can also check for accessability and security tags like sandbox, title, lazy, etc + if (!iframe || parentDiv.children.length !== 1) { + return { isValid: false, message: 'Input must contain a single iframe tag.' } + } + + const src = iframe.getAttribute('src') + if (!src) { + return { isValid: false, message: 'Iframe must have a valid src attribute.' } + } + + const urlValidationResponse = validateURL(src) + + if (!urlValidationResponse.isValid) { + return urlValidationResponse + } + + return { isValid: true } +}