diff --git a/console/locales/en/plugin__patterns-operator-console-plugin.json b/console/locales/en/plugin__patterns-operator-console-plugin.json index e6ce26dd1..70f805afe 100644 --- a/console/locales/en/plugin__patterns-operator-console-plugin.json +++ b/console/locales/en/plugin__patterns-operator-console-plugin.json @@ -12,9 +12,11 @@ "Catalog source": "Catalog source", "Checking vault status": "Checking vault status", "Cluster": "Cluster", + "Compute": "Compute", "Configure secrets for this pattern. File and INI fields are required before install.": "Configure secrets for this pattern. File and INI fields are required before install.", "Configure secrets that will be injected into Vault for this pattern.": "Configure secrets that will be injected into Vault for this pattern.", "Confirm Uninstall": "Confirm Uninstall", + "Control plane": "Control plane", "Current Step": "Current Step", "Deleting": "Deleting", "Deletion Progress": "Deletion Progress", @@ -51,6 +53,7 @@ "Loading secret template": "Loading secret template", "Manage Secrets": "Manage Secrets", "Manage Secrets for {{displayName}}": "Manage Secrets for {{displayName}}", + "More information about tested requirements": "More information about tested requirements", "Name": "Name", "Namespace": "Namespace", "No pattern specified": "No pattern specified", @@ -82,7 +85,8 @@ "Target Repo": "Target Repo", "Target Revision": "Target Revision", "Target section: {{section}}": "Target section: {{section}}", - "Tested Requirements:": "Tested Requirements:", + "Tested requirements": "Tested requirements", + "Tested requirements details": "Tested requirements details", "The pattern and all its associated resources have been fully deleted.": "The pattern and all its associated resources have been fully deleted.", "The vault injection job has been created.": "The vault injection job has been created.", "This is the sizing that has been tested. The pattern is expected to work on any similarly-sized architecture.": "This is the sizing that has been tested. The pattern is expected to work on any similarly-sized architecture.", diff --git a/console/package.json b/console/package.json index 4ca877418..03294da84 100644 --- a/console/package.json +++ b/console/package.json @@ -37,6 +37,7 @@ "@types/react": "^17.0.37", "@types/react-helmet": "^6.1.4", "@types/react-router-dom": "^5.3.2", + "@types/sanitize-html": "^2.16.1", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "concurrently": "^10.0.3", @@ -96,6 +97,7 @@ }, "dependencies": { "js-yaml": "^4.3.2", + "sanitize-html": "^2.17.7", "yaml": "^2.8.3" }, "resolutions": { diff --git a/console/src/components/PatternCard.css b/console/src/components/PatternCard.css new file mode 100644 index 000000000..b7f2e80dd --- /dev/null +++ b/console/src/components/PatternCard.css @@ -0,0 +1,21 @@ +.patterns-operator__pattern-logo { + max-height: 32px; + object-fit: contain; +} + +.patterns-operator__card-header { + height: 32px; +} + +.patterns-operator__pattern-description { + min-height: 105px; +} + +.patterns-operator__card--disabled { + opacity: 0.5; + pointer-events: none; +} + +.patterns-operator__card--disabled .patterns-operator__card-actions { + pointer-events: auto; +} \ No newline at end of file diff --git a/console/src/components/PatternCard.tsx b/console/src/components/PatternCard.tsx new file mode 100644 index 000000000..7b7404321 --- /dev/null +++ b/console/src/components/PatternCard.tsx @@ -0,0 +1,332 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, + CardTitle, + Flex, + FlexItem, + Label, + LabelGroup, + Popover, + Stack, + StackItem, + Tooltip, +} from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { + ExternalLinkAltIcon, + InfoCircleIcon, + OutlinedQuestionCircleIcon, +} from '@patternfly/react-icons'; +import { Pattern, ClusterRoleRequirements, NodeRequirement } from '../types'; +import { PatternTierLabel } from './PatternTierLabel'; +import './PatternCard.css'; + +type CloudLabelKey = 'aws' | 'gcp' | 'azure'; + +const CLOUD_LABELS: Record = { + aws: 'AWS', + gcp: 'GCP', + azure: 'Azure', +}; + +function getCloudProviders(pattern: Pattern): string[] { + if (!pattern.requirements) return []; + const providers = new Set(); + for (const role of Object.values(pattern.requirements)) { + for (const nodeType of [role.compute, role.controlPlane]) { + if (nodeType) { + Object.keys(nodeType).forEach((p) => providers.add(p)); + } + } + } + return Array.from(providers); +} + +function formatNode(node?: NodeRequirement): string { + return node && node.replicas !== 0 ? `${node.replicas} × ${node.type}` : '-'; +} + +function getSizingSummary(role: ClusterRoleRequirements, cloud: string): string | null { + const control = role.controlPlane?.[cloud]; + const compute = role.compute?.[cloud]; + if (!control && !compute) return null; + const parts: string[] = []; + if (control) parts.push(`${control.replicas} control`); + if (compute) parts.push(`${compute.replicas} compute`); + return parts.join(' + '); +} + +function HubSummary({ pattern, clouds }: { pattern: Pattern; clouds: string[] }) { + const { t } = useTranslation('plugin__patterns-operator-console-plugin'); + const hub = pattern.requirements?.hub; + const spoke = pattern.requirements?.spoke; + const defaultCloud = clouds.includes('aws') ? 'aws' : clouds[0]; + const hubSummary = hub && defaultCloud ? getSizingSummary(hub, defaultCloud) : null; + const spokeSummary = spoke && defaultCloud ? getSizingSummary(spoke, defaultCloud) : null; + + if (!hubSummary) return null; + + return ( +
+
+ {spoke ? ( + <> + {t('Hub')}: {hubSummary} +
+ {t('Spoke')}: {spokeSummary} + + ) : ( + `${t('Cluster')}: ${hubSummary}` + )} +
+ {pattern.external_requirements?.cluster_sizing_note && ( + + + {t('Additional requirements')} + + + )} +
+ ); +} + +function RequirementsPopoverBody({ pattern, clouds }: { pattern: Pattern; clouds: string[] }) { + const { t } = useTranslation('plugin__patterns-operator-console-plugin'); + const hub = pattern.requirements?.hub; + const spoke = pattern.requirements?.spoke; + + const roles: { label: string; role: ClusterRoleRequirements }[] = []; + if (hub) roles.push({ label: spoke ? t('Hub') : t('Cluster'), role: hub }); + if (spoke) roles.push({ label: t('Spoke'), role: spoke }); + + return ( + + + {t( + 'This is the sizing that has been tested. The pattern is expected to work on any similarly-sized architecture.', + )} + + {clouds.map((cloud) => ( + + + + + + + + + + + {roles.map(({ label, role }) => ( + + + + + + ))} + +
+ {t('Control plane')}{t('Compute')}
+ {label} + + {formatNode(role.controlPlane?.[cloud])} + + {formatNode(role.compute?.[cloud])} +
+
+ ))} +
+ ); +} + +type PatternCardProps = { + pattern: Pattern; + isInstalled: boolean; + isDisabled: boolean; + navigate: (path: string) => void; +}; + +export default function PatternCard({ + pattern, + isInstalled, + isDisabled, + navigate, +}: PatternCardProps) { + const { t } = useTranslation('plugin__patterns-operator-console-plugin'); + const [isVisible, setIsVisible] = React.useState(false); + const clouds = getCloudProviders(pattern); + + return ( + + + + + + + {isInstalled && } + + + + {pattern.logo ? ( + {`${pattern.display_name} + ) : null} + + + + + + {pattern.display_name} + + + + + {pattern.description && ( + +
{pattern.description}
+
+ )} + + + + + + + Tested requirements + + + + setIsVisible(true)} + shouldClose={(_event, _fn) => setIsVisible(false)} + headerContent={t('Tested requirements')} + bodyContent={} + > + + + )} + {pattern.repo_url && ( + + + + )} + + + )} + +
+ {isInstalled ? ( + + + + + ) : ( + + + + )} +
+
+
+ +
+ ); +} diff --git a/console/src/components/PatternCatalogPage.css b/console/src/components/PatternCatalogPage.css index 407b43f33..42d97e600 100644 --- a/console/src/components/PatternCatalogPage.css +++ b/console/src/components/PatternCatalogPage.css @@ -8,78 +8,3 @@ max-height: 48px; object-fit: contain; } - -.patterns-operator__pattern-logo { - max-height: 32px; - object-fit: contain; -} - -.patterns-operator__card-field { - margin-bottom: 4px; -} - -.patterns-operator__installed-label { - margin-left: 8px; -} - -.patterns-operator__requirements { - display: flex; - flex-direction: column; - gap: 4px; -} - -.patterns-operator__requirements-heading { - font-size: 0.8rem; - font-weight: 600; - color: var(--pf-v5-global--Color--100, var(--pf-v6-global--Color--100, #151515)); - cursor: pointer; -} - -.patterns-operator__cloud-labels { - display: flex; - gap: 4px; - flex-wrap: wrap; -} - -.patterns-operator__sizing-line { - font-size: 0.875rem; - color: var(--pf-v5-global--Color--200, var(--pf-v6-global--Color--200, #6a6e73)); - cursor: pointer; -} - -.patterns-operator__sizing-note { - font-size: 0.875rem; - color: var(--pf-v5-global--Color--200, var(--pf-v6-global--Color--200, #6a6e73)); - cursor: pointer; -} - -.patterns-operator__card-description { - font-size: 0.875rem; - color: var(--pf-v5-global--Color--200, var(--pf-v6-global--Color--200, #6a6e73)); -} - -.patterns-operator__card-footer { - display: flex; - flex-direction: column; - gap: 8px; -} - -.patterns-operator__card-links { - display: flex; - gap: 16px; -} - -.patterns-operator__card-actions { - display: flex; - gap: 16px; -} - -.patterns-operator__card--disabled { - opacity: 0.5; - pointer-events: none; -} - -.patterns-operator__card--disabled .patterns-operator__card-actions { - pointer-events: auto; -} - diff --git a/console/src/components/PatternCatalogPage.tsx b/console/src/components/PatternCatalogPage.tsx index 579bbd968..6ead5cf71 100644 --- a/console/src/components/PatternCatalogPage.tsx +++ b/console/src/components/PatternCatalogPage.tsx @@ -5,14 +5,7 @@ import { useNavigateCompat } from '../hooks/useNavigateCompat'; import { Alert, - Button, - Card, - CardBody, - CardFooter, - CardHeader, - CardTitle, Gallery, - Label, MenuToggle, PageSection, Select, @@ -25,145 +18,30 @@ import { ToolbarItem, Tooltip, } from '@patternfly/react-core'; -import { ExternalLinkAltIcon, InfoCircleIcon } from '@patternfly/react-icons'; import { fetchAllPatterns, fetchInstalledPatterns, fetchCatalogImage } from '../api'; -import { Pattern, ClusterRoleRequirements } from '../types'; +import { Pattern } from '../types'; import './PatternCatalogPage.css'; - -const ALLOWED_TAGS = ['b', 'i', 'em', 'strong', 'a', 'br']; - -function sanitizeHTML(html: string): string { - return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g, (match, tag) => { - const lower = tag.toLowerCase(); - if (!ALLOWED_TAGS.includes(lower)) return ''; - if (lower === 'a') { - const hrefMatch = match.match(/href\s*=\s*"([^"]*)"/); - if (match.startsWith(''; - return hrefMatch - ? `` - : ''; - } - return match.startsWith('` : `<${lower}>`; +import PatternCard from './PatternCard'; +import useLocalStorage from '../hooks/useLocalStorage'; +import sanitizeHtml from 'sanitize-html'; + +function sanitize(html: string): string { + return sanitizeHtml(html, { + allowedTags: ['b', 'i', 'em', 'strong', 'a', 'br'], + allowedAttributes: { + a: ['href'], + }, + transformTags: { + a: sanitizeHtml.simpleTransform('a', { + target: '_blank', + rel: 'noopener noreferrer', + }), + }, }); } -const CLOUD_LABELS: Record = { - aws: 'AWS', - gcp: 'GCP', - azure: 'Azure', -}; - -function getCloudProviders(pattern: Pattern): string[] { - if (!pattern.requirements) return []; - const providers = new Set(); - for (const role of Object.values(pattern.requirements)) { - for (const nodeType of [role.compute, role.controlPlane]) { - if (nodeType) { - Object.keys(nodeType).forEach((p) => providers.add(p)); - } - } - } - return Array.from(providers); -} - -function getSizingSummary(role: ClusterRoleRequirements, cloud: string): string | null { - const control = role.controlPlane?.[cloud]; - const compute = role.compute?.[cloud]; - if (!control && !compute) return null; - const parts: string[] = []; - if (control) parts.push(`${control.replicas} control`); - if (compute) parts.push(`${compute.replicas} compute`); - return parts.join(' + '); -} - -function formatRoleLine(role: ClusterRoleRequirements, cloud: string): string { - const control = role.controlPlane?.[cloud]; - const compute = role.compute?.[cloud]; - const parts: string[] = []; - if (control) parts.push(`${control.replicas}× ${control.type} control`); - if (compute) parts.push(`${compute.replicas}× ${compute.type} compute`); - return parts.join(', '); -} - -function getRequirementsTooltip( - hub: ClusterRoleRequirements | undefined, - spoke: ClusterRoleRequirements | undefined, - clouds: string[], -): string { - return clouds - .map((cloud) => { - const lines: string[] = []; - const hubLine = hub ? formatRoleLine(hub, cloud) : ''; - const spokeLine = spoke ? formatRoleLine(spoke, cloud) : ''; - if (hubLine) lines.push(spoke ? ` Hub: ${hubLine}` : ` ${hubLine}`); - if (spokeLine) lines.push(` Spoke: ${spokeLine}`); - return lines.length ? `${CLOUD_LABELS[cloud] || cloud}\n${lines.join('\n')}` : null; - }) - .filter(Boolean) - .join('\n'); -} - -const TIER_COLORS: Record = { - maintained: 'green', - tested: 'blue', - sandbox: 'orange', -}; - -const TIER_SVG_COLORS: Record = { - maintained: { filled: '#3e8635', outline: '#3e8635' }, - tested: { filled: '#0066cc', outline: '#0066cc' }, - sandbox: { filled: '#f0ab00', outline: '#f0ab00' }, -}; - const KNOWN_TIER_ORDER = ['maintained', 'tested', 'sandbox']; -const TIER_FILLED_BARS: Record = { - maintained: 3, - tested: 2, - sandbox: 1, -}; - -function TierIcon({ tier }: { tier: string }): React.ReactElement | null { - const colors = TIER_SVG_COLORS[tier]; - if (!colors) return null; - const filledCount = TIER_FILLED_BARS[tier] ?? 1; - return ( - - {[0, 1, 2].map((i) => { - const y = 34 - i * 14; - const filled = i < filledCount; - return ( - - ); - })} - - ); -} - -const TIER_DESCRIPTIONS: Record = { - maintained: - 'Rigorously tested through an automated CI pipeline with continuous validation across OpenShift versions. Highest level of validation and prioritized for ongoing maintenance.', - tested: - 'Undergoes a manual or automated test plan which passes at least once for each new OpenShift Container Platform minor version.', - sandbox: - 'Entry-level patterns that are deployable onto a freshly installed OpenShift cluster without prior modification. May be work-in-progress.', -}; - export default function PatternCatalogPage() { const { t } = useTranslation('plugin__patterns-operator-console-plugin'); const navigate = useNavigateCompat(); @@ -174,7 +52,10 @@ export default function PatternCatalogPage() { const [catalogImage, setCatalogImage] = React.useState(null); const [catalogDescription, setCatalogDescription] = React.useState(); const [catalogLogo, setCatalogLogo] = React.useState(); - const [selectedTiers, setSelectedTiers] = React.useState>(new Set()); + const [storedTiers, setStoredTiers] = useLocalStorage( + 'patterns-operator__catalog-selected-tiers', + null, + ); const [tierSelectOpen, setTierSelectOpen] = React.useState(false); const loadData = React.useCallback(() => { @@ -210,19 +91,17 @@ export default function PatternCatalogPage() { }); }, [patterns]); - const defaultsApplied = React.useRef(false); - React.useEffect(() => { - if (defaultsApplied.current || availableTiers.length === 0) return; - defaultsApplied.current = true; - if (availableTiers.includes('maintained')) { - setSelectedTiers(new Set(['maintained'])); - } else { - setSelectedTiers(new Set(availableTiers)); - } - }, [availableTiers]); + const defaultTiers = React.useMemo( + () => (availableTiers.includes('maintained') ? ['maintained'] : availableTiers), + [availableTiers], + ); + const selectedTiers = storedTiers ?? defaultTiers; const filteredPatterns = React.useMemo( - () => (selectedTiers.size === 0 ? patterns : patterns.filter((p) => selectedTiers.has(p.tier))), + () => + selectedTiers.length === 0 + ? patterns + : patterns.filter((p) => selectedTiers.includes(p.tier)), [patterns, selectedTiers], ); @@ -230,23 +109,18 @@ export default function PatternCatalogPage() { _event: React.MouseEvent | undefined, value: string | number | undefined, ) => { - setSelectedTiers((prev) => { - const next = new Set(prev); - if (next.has(value as string)) { - next.delete(value as string); - } else { - next.add(value as string); - } - return next; - }); + const tier = value as string; + setStoredTiers( + selectedTiers.includes(tier) + ? selectedTiers.filter((t) => t !== tier) + : [...selectedTiers, tier], + ); }; const tierToggleLabel = - selectedTiers.size === 0 + selectedTiers.length === 0 ? t('Tier') - : Array.from(selectedTiers) - .map((tier) => tier.charAt(0).toUpperCase() + tier.slice(1)) - .join(', '); + : selectedTiers.map((tier) => tier.charAt(0).toUpperCase() + tier.slice(1)).join(', '); return ( <> @@ -281,7 +155,7 @@ export default function PatternCatalogPage() { {catalogDescription && ( -

+

)} @@ -300,7 +174,7 @@ export default function PatternCatalogPage() { role="menu" id="tier-filter" isOpen={tierSelectOpen} - selected={Array.from(selectedTiers)} + selected={selectedTiers} onSelect={onTierSelect} onOpenChange={setTierSelectOpen} toggle={(toggleRef) => ( @@ -320,7 +194,7 @@ export default function PatternCatalogPage() { key={tier} value={tier} hasCheckbox - isSelected={selectedTiers.has(tier)} + isSelected={selectedTiers.includes(tier)} > {tier.charAt(0).toUpperCase() + tier.slice(1)} @@ -330,199 +204,19 @@ export default function PatternCatalogPage() { - + {filteredPatterns.map((pattern) => { const isInstalled = installedPatterns.has(pattern.name); const hasAnyInstalled = installedPatterns.size > 0; const isDisabled = hasAnyInstalled && !isInstalled; return ( - - - ), - hasNoOffset: true, - } - : undefined - } - > - - - - {isInstalled && ( - - )} - - - {pattern.display_name} - - - {pattern.description && ( -

- {pattern.description} -
- )} - - - {pattern.requirements && - (() => { - const clouds = getCloudProviders(pattern); - const hub = pattern.requirements.hub; - const spoke = pattern.requirements.spoke; - const defaultCloud = clouds.includes('aws') ? 'aws' : clouds[0]; - const hubSummary = - hub && defaultCloud ? getSizingSummary(hub, defaultCloud) : null; - const spokeSummary = - spoke && defaultCloud ? getSizingSummary(spoke, defaultCloud) : null; - const fullTooltip = getRequirementsTooltip(hub, spoke, clouds); - return ( -
- -
- {t('Tested Requirements:')} -
-
- {clouds.length > 0 && ( -
- {clouds.map((cloud) => ( - - ))} -
- )} - {hubSummary && ( - - {fullTooltip} - - } - > -
- {spoke ? ( - <> - {t('Hub')}: {hubSummary} -
- {t('Spoke')}: {spokeSummary} - - ) : ( - `${t('Cluster')}: ${hubSummary}` - )} -
-
- )} - {pattern.external_requirements?.cluster_sizing_note && ( - - - {t('Additional requirements')} - - - )} -
- ); - })()} -
- - {(pattern.docs_url || pattern.repo_url) && ( -
- {pattern.docs_url && ( - - )} - {pattern.repo_url && ( - - )} -
- )} -
- {isInstalled && ( - - )} - {isInstalled && ( - - )} - {!isInstalled && ( - - - - )} -
-
- + pattern={pattern} + isInstalled={isInstalled} + isDisabled={isDisabled} + navigate={navigate} + /> ); })} diff --git a/console/src/components/PatternTierLabel.tsx b/console/src/components/PatternTierLabel.tsx new file mode 100644 index 000000000..1e7b7b7a0 --- /dev/null +++ b/console/src/components/PatternTierLabel.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; + +import { Label, Tooltip } from '@patternfly/react-core'; + +const TIER_COLORS: Record = { + maintained: 'green', + tested: 'blue', + sandbox: 'orange', +}; + +const TIER_SVG_COLORS: Record = { + maintained: { filled: '#3e8635', outline: '#3e8635' }, + tested: { filled: '#0066cc', outline: '#0066cc' }, + sandbox: { filled: '#f0ab00', outline: '#f0ab00' }, +}; + +const TIER_DESCRIPTIONS: Record = { + maintained: + 'Rigorously tested through an automated CI pipeline with continuous validation across OpenShift versions. Highest level of validation and prioritized for ongoing maintenance.', + tested: + 'Undergoes a manual or automated test plan which passes at least once for each new OpenShift Container Platform minor version.', + sandbox: + 'Entry-level patterns that are deployable onto a freshly installed OpenShift cluster without prior modification. May be work-in-progress.', +}; + +const TIER_FILLED_BARS: Record = { + maintained: 3, + tested: 2, + sandbox: 1, +}; + +function TierIcon({ tier }: { tier: string }): React.ReactElement | null { + const colors = TIER_SVG_COLORS[tier]; + if (!colors) return null; + const filledCount = TIER_FILLED_BARS[tier] ?? 1; + return ( + + {[0, 1, 2].map((i) => { + const y = 34 - i * 14; + const filled = i < filledCount; + return ( + + ); + })} + + ); +} + +export function PatternTierLabel({ tier }: { tier: string }): React.ReactElement { + return ( + + + + ); +} diff --git a/console/src/hooks/useLocalStorage.ts b/console/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..d77731ce0 --- /dev/null +++ b/console/src/hooks/useLocalStorage.ts @@ -0,0 +1,52 @@ +import { useCallback, useState } from "react"; + +interface StoredValue { + version: number; + value: T; +} + +function readValue(key: string, initialValue: T, version: number): T { + try { + const item = window.localStorage.getItem(key); + if (item === null) return initialValue; + + const parsed: StoredValue = JSON.parse(item); + if (parsed.version !== version) return initialValue; + + return parsed.value; + } catch { + return initialValue; + } +} + +function saveValue(key: string, value: T, version: number): void { + try { + const toStore: StoredValue = { version, value }; + window.localStorage.setItem(key, JSON.stringify(toStore)); + } catch { + // Silently fail on quota exceeded or other storage errors + } +} + +export default function useLocalStorage( + key: string, + initialValue: T, + version = 1, +): [T, (value: T | ((prev: T) => T)) => void] { + const [storedValue, setStoredValue] = useState(() => + readValue(key, initialValue, version), + ); + + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + setStoredValue((prev) => { + const newValue = value instanceof Function ? value(prev) : value; + saveValue(key, newValue, version); + return newValue; + }); + }, + [key, version], + ); + + return [storedValue, setValue]; +} \ No newline at end of file diff --git a/console/yarn.lock b/console/yarn.lock index ecdfa7d46..5f7a0c033 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -1249,6 +1249,13 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== +"@types/sanitize-html@^2.16.1": + version "2.16.1" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.16.1.tgz#27b9ac6cc29838f7a048bfec0113e8ad00918d0a" + integrity sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA== + dependencies: + htmlparser2 "^10.1" + "@types/scheduler@^0.16": version "0.16.8" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" @@ -2960,6 +2967,11 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== +dayjs@^1.11.7: + version "1.11.23" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.23.tgz#b0a363506dde5f36cf5075e42ebe8115165a8c79" + integrity sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ== + debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3004,6 +3016,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + default-browser-id@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" @@ -3119,11 +3136,25 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" +dom-serializer@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-3.1.1.tgz#54be70ee4fcc2da010f488165e62294798dc57d3" + integrity sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw== + dependencies: + domelementtype "^3.0.0" + domhandler "^6.0.0" + entities "^8.0.0" + domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domelementtype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-3.0.0.tgz#e6c0a24bd39ca5eb7ea67a98d2f699497d7bcc55" + integrity sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg== + domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" @@ -3131,6 +3162,13 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" +domhandler@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-6.0.1.tgz#75f351a03a6e10c35e08418f9cf0d287e00797cf" + integrity sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg== + dependencies: + domelementtype "^3.0.0" + domutils@^3.0.1, domutils@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" @@ -3140,6 +3178,15 @@ domutils@^3.0.1, domutils@^3.2.2: domelementtype "^2.3.0" domhandler "^5.0.3" +domutils@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-4.0.2.tgz#0c1ac1fdbe8f554a60a6f5eb143ee85630327c94" + integrity sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA== + dependencies: + dom-serializer "^3.0.0" + domelementtype "^3.0.0" + domhandler "^6.0.0" + dotenv@^16.0.0: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" @@ -3248,6 +3295,11 @@ entities@^7.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== +entities@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-8.1.0.tgz#9632d69619ebdbe0cf0ef22f53088fe5a2163d86" + integrity sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA== + envinfo@^7.7.3: version "7.21.0" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae" @@ -4367,7 +4419,7 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== -htmlparser2@^10.1.0: +htmlparser2@^10.1, htmlparser2@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.1.0.tgz#fe3f2e12c73b6e462d4e10395db9c1119e4d6ae4" integrity sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ== @@ -4377,6 +4429,16 @@ htmlparser2@^10.1.0: domutils "^3.2.2" entities "^7.0.1" +htmlparser2@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-12.0.0.tgz#6a679d0f57c525990f9cbad8a585b320ecc6d198" + integrity sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw== + dependencies: + domelementtype "^3.0.0" + domhandler "^6.0.0" + domutils "^4.0.2" + entities "^8.0.0" + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -5067,6 +5129,13 @@ launch-editor@^2.14.1: picocolors "^1.1.1" shell-quote "^1.8.4" +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -5513,6 +5582,11 @@ nanoid@^3.3.16: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c" integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q== +nanoid@^3.3.18: + version "3.3.19" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.19.tgz#336d4aa4bcd4fb24d2cddede7ffeae40bec03f0a" + integrity sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -5799,6 +5873,11 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + parse5-htmlparser2-tree-adapter@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b" @@ -6004,6 +6083,15 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@^8.3.11: + version "8.5.28" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.28.tgz#da4563a99a06e62d6c1cd1acae363224bcaed6e9" + integrity sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A== + dependencies: + nanoid "^3.3.18" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postcss@^8.4.28, postcss@^8.4.33: version "8.5.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594" @@ -6614,6 +6702,19 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@^2.17.7: + version "2.17.7" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.7.tgz#b16417c348ea5f99c2451964b6e6621ca6b1da94" + integrity sha512-PGtEkc9cbnedU3s9TmzDbpsZ8w086g/0Q8k8/oIO1NLNU3i5k9yn835CrjJSajp1KMmkisbO1qPXxNKO3welAg== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^12.0.0" + is-plain-object "^5.0.0" + launder "^1.7.1" + parse-srcset "^1.0.2" + postcss "^8.3.11" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"