fix: invalid URL or protocol not validated consistently by server and UI (#27052)

Signed-off-by: Atif Ali <atali@redhat.com>
This commit is contained in:
Atif Ali
2026-03-27 23:15:03 -04:00
committed by GitHub
parent 94d8ba92a8
commit 759e746e87
13 changed files with 309 additions and 53 deletions

View File

@@ -62,6 +62,17 @@ func SanitizeCluster(cluster *v1alpha1.Cluster) (*unstructured.Unstructured, err
})
}
func managedByURLFromAnnotations(annotations map[string]any) (string, bool) {
managedByURL, ok := annotations[v1alpha1.AnnotationKeyManagedByURL].(string)
if !ok {
return "", false
}
if err := settings.ValidateExternalURL(managedByURL); err != nil {
return "", false
}
return managedByURL, true
}
func CreateDeepLinksObject(resourceObj *unstructured.Unstructured, app *unstructured.Unstructured, cluster *unstructured.Unstructured, project *unstructured.Unstructured) map[string]any {
deeplinkObj := map[string]any{}
if resourceObj != nil {
@@ -72,12 +83,10 @@ func CreateDeepLinksObject(resourceObj *unstructured.Unstructured, app *unstruct
deeplinkObj[AppDeepLinkShortKey] = app.Object
// Add managed-by URL if present in annotations
if app.Object["metadata"] != nil {
if metadata, ok := app.Object["metadata"].(map[string]any); ok {
if annotations, ok := metadata["annotations"].(map[string]any); ok {
if managedByURL, ok := annotations[v1alpha1.AnnotationKeyManagedByURL].(string); ok {
deeplinkObj[ManagedByURLKey] = managedByURL
}
if metadata, ok := app.Object["metadata"].(map[string]any); ok {
if annotations, ok := metadata["annotations"].(map[string]any); ok {
if managedByURL, ok := managedByURLFromAnnotations(annotations); ok {
deeplinkObj[ManagedByURLKey] = managedByURL
}
}
}

View File

@@ -237,6 +237,29 @@ func TestManagedByURLAnnotation(t *testing.T) {
assert.Equal(t, managedByURL, deeplinksObj[ManagedByURLKey])
})
t.Run("application with invalid managed-by-url annotation is omitted", func(t *testing.T) {
// Non http(s) protocols are invalid and should not be used in deep link generation.
managedByURL := "ftp://localhost:8081"
app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Annotations: map[string]string{
v1alpha1.AnnotationKeyManagedByURL: managedByURL,
},
},
}
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app)
require.NoError(t, err)
unstructuredObj := &unstructured.Unstructured{Object: obj}
deeplinksObj := CreateDeepLinksObject(nil, unstructuredObj, nil, nil)
_, exists := deeplinksObj[ManagedByURLKey]
assert.False(t, exists)
})
t.Run("application without managed-by-url annotation", func(t *testing.T) {
// Create an application without managed-by-url annotation
app := &v1alpha1.Application{

View File

@@ -12,13 +12,16 @@ import {
createdOrNodeKey,
resourceStatusToResourceNode,
getApplicationLinkURLFromNode,
getManagedByURLFromNode
getManagedByURLFromNode,
MANAGED_BY_URL_INVALID_TEXT,
MANAGED_BY_URL_INVALID_COLOR
} from '../utils';
import {AppDetailsPreferences} from '../../../shared/services';
import {Consumer} from '../../../shared/context';
import Moment from 'react-moment';
import {format} from 'date-fns';
import {HealthPriority, ResourceNode, SyncPriority, SyncStatusCode} from '../../../shared/models';
import {isValidManagedByURL} from '../../../shared/utils';
import './application-resource-list.scss';
export interface ApplicationResourceListProps {
@@ -201,6 +204,20 @@ export const ApplicationResourceList = (props: ApplicationResourceListProps) =>
? getApplicationLinkURLFromNode(node, ctx.baseHref)
: {url: ctx.baseHref + 'applications/' + res.namespace + '/' + res.name, isExternal: false};
const managedByURL = node ? getManagedByURLFromNode(node) : null;
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
className='application-details__external_link'
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => {
e.stopPropagation();
}}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<span className='application-details__external_link'>
<a

View File

@@ -6,6 +6,7 @@ import Moment from 'react-moment';
import * as moment from 'moment';
import * as models from '../../../shared/models';
import {isValidManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_COLOR} from '../../../shared/utils';
import {EmptyState} from '../../../shared/components';
import {AppContext, Consumer} from '../../../shared/context';
@@ -496,6 +497,20 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
{ctx => {
// For nested applications, use the node's data to construct the URL
const linkInfo = getApplicationLinkURLFromNode(node, ctx.baseHref);
const managedByURL = getManagedByURLFromNode(node);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
role='link'
aria-disabled={true}
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => e.stopPropagation()}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<a
href={linkInfo.url}
@@ -504,7 +519,7 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
onClick={e => {
e.stopPropagation();
}}
title={getManagedByURLFromNode(node) ? `Open application\nmanaged-by-url: ${getManagedByURLFromNode(node)}` : 'Open application'}>
title={managedByURL ? `Open application\nmanaged-by-url: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</a>
);
@@ -806,6 +821,20 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
{ctx => {
// For nested applications, use the node's data to construct the URL
const linkInfo = getApplicationLinkURLFromNode(node, ctx.baseHref);
const managedByURL = getManagedByURLFromNode(node);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
if (managedByURLInvalid) {
return (
<span
role='link'
aria-disabled={true}
style={{cursor: 'not-allowed', display: 'inline-flex', alignItems: 'center'}}
onClick={e => e.stopPropagation()}
title={`Open application\n${MANAGED_BY_URL_INVALID_TEXT}`}>
<i className='fa fa-external-link-alt' style={{color: MANAGED_BY_URL_INVALID_COLOR}} />
</span>
);
}
return (
<a
href={linkInfo.url}
@@ -814,7 +843,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
onClick={e => {
e.stopPropagation();
}}
title={getManagedByURLFromNode(node) ? `Open application\nmanaged-by-url: ${getManagedByURLFromNode(node)}` : 'Open application'}>
title={managedByURL ? `Open application\nmanaged-by-url: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</a>
);

View File

@@ -1,4 +1,4 @@
import {DropDownMenu, Tooltip} from 'argo-ui';
import {DropDownMenu, NotificationType, Tooltip} from 'argo-ui';
import * as React from 'react';
import Moment from 'react-moment';
import {Cluster} from '../../../shared/components';
@@ -6,7 +6,8 @@ import {ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import {ApplicationURLs} from '../application-urls';
import * as AppUtils from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL} from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {isValidManagedByURL} from '../../../shared/utils';
import {ApplicationsLabels} from './applications-labels';
import {ApplicationsSource} from './applications-source';
import {services} from '../../../shared/services';
@@ -27,6 +28,8 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
const healthStatus = app.status.health.status;
const linkInfo = getApplicationLinkURL(app, ctx.baseHref);
const source = getAppDefaultSource(app);
const managedByURL = getManagedByURL(app);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -40,6 +43,18 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -92,9 +107,11 @@ export const ApplicationTableRow = ({app, selected, pref, ctx, syncApplication,
<span>{app.metadata.name}</span>
</Tooltip>
<button
type='button'
className={managedByURLInvalid ? 'managed-by-url-invalid' : undefined}
onClick={handleExternalLinkClick}
style={{marginLeft: '0.5em'}}
title={`Link: ${linkInfo.url}\nmanaged-by-url: ${getManagedByURL(app) || 'none'}`}>
style={{marginLeft: '0.5em', cursor: managedByURLInvalid ? 'not-allowed' : undefined}}
title={managedByURLInvalid ? MANAGED_BY_URL_INVALID_TEXT : `Link: ${linkInfo.url}\nmanaged-by-url: ${managedByURL || 'none'}`}>
<i className='fa fa-external-link-alt' />
</button>
</div>

View File

@@ -1,4 +1,4 @@
import {Tooltip} from 'argo-ui';
import {NotificationType, Tooltip} from 'argo-ui';
import * as classNames from 'classnames';
import * as React from 'react';
import {Cluster} from '../../../shared/components';
@@ -6,7 +6,8 @@ import {ContextApis, AuthSettingsCtx} from '../../../shared/context';
import * as models from '../../../shared/models';
import {ApplicationURLs} from '../application-urls';
import * as AppUtils from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL} from '../utils';
import {getAppDefaultSource, OperationState, getApplicationLinkURL, getManagedByURL, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {isValidManagedByURL} from '../../../shared/utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
@@ -30,6 +31,8 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
const targetRevision = source ? source.targetRevision || 'HEAD' : 'Unknown';
const linkInfo = getApplicationLinkURL(app, ctx.baseHref);
const healthStatus = app.status.health.status;
const managedByURL = getManagedByURL(app);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -43,6 +46,18 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -67,9 +82,20 @@ export const ApplicationTile = ({app, selected, pref, ctx, tileRef, syncApplicat
<div className={app.status.summary?.externalURLs?.length > 0 ? 'columns small-2' : 'columns small-1'}>
<div className='applications-list__external-link'>
<ApplicationURLs urls={app.status.summary?.externalURLs} />
<button onClick={handleExternalLinkClick} title={getManagedByURL(app) ? `Managed by: ${getManagedByURL(app)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
{managedByURLInvalid ? (
<button
type='button'
className='managed-by-url-invalid'
onClick={handleExternalLinkClick}
style={{cursor: 'not-allowed'}}
title={MANAGED_BY_URL_INVALID_TEXT}>
<i className='fa fa-external-link-alt' />
</button>
) : (
<button type='button' onClick={handleExternalLinkClick} title={managedByURL ? `Managed by: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
)}
<button
title={favList?.includes(app.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}
className='large-text-height'

View File

@@ -39,28 +39,36 @@
flex: 1;
min-width: 0;
}
.applications-table-source__labels {
max-width: 40%;
}
}
.applications-list__external-link {
button {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
color: inherit;
&:hover {
color: $argo-color-teal-5;
.applications-table-source__labels {
max-width: 40%;
}
i {
font-size: 14px;
}
.applications-list__external-link {
button {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
color: inherit;
&:hover {
color: $argo-color-teal-5;
}
i {
font-size: 14px;
}
}
}
.applications-list__table-row button.managed-by-url-invalid {
color: #f4c030;
&:hover {
color: #f4c030;
}
}
}
}

View File

@@ -70,9 +70,26 @@
&:hover {
color: $argo-color-teal-5;
}
&.managed-by-url-invalid {
color: #f4c030;
&:hover {
color: #f4c030;
}
}
i {
font-size: 14px;
}
}
}
/* Table / name column external-link (not under .applications-list__external-link) */
.applications-list__table-row button.managed-by-url-invalid {
color: #f4c030;
&:hover {
color: #f4c030;
}
}

View File

@@ -1,12 +1,13 @@
import {Tooltip} from 'argo-ui';
import {NotificationType, Tooltip} from 'argo-ui';
import * as React from 'react';
import Moment from 'react-moment';
import {ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import * as AppUtils from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus} from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
import {isValidManagedByURL} from '../../../shared/utils';
export interface AppSetTableRowProps {
appSet: models.ApplicationSet;
@@ -19,6 +20,8 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
const favList = pref.appList.favoritesAppList || [];
const healthStatus = getAppSetHealthStatus(appSet);
const linkInfo = getApplicationLinkURL(appSet, ctx.baseHref);
const managedByURL = getManagedByURL(appSet);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -32,6 +35,18 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -81,9 +96,11 @@ export const AppSetTableRow = ({appSet, selected, pref, ctx}: AppSetTableRowProp
<span>{appSet.metadata.name}</span>
</Tooltip>
<button
type='button'
className={managedByURLInvalid ? 'managed-by-url-invalid' : undefined}
onClick={handleExternalLinkClick}
style={{marginLeft: '0.5em'}}
title={`Link: ${linkInfo.url}\nmanaged-by-url: ${getManagedByURL(appSet) || 'none'}`}>
style={{marginLeft: '0.5em', cursor: managedByURLInvalid ? 'not-allowed' : undefined}}
title={managedByURLInvalid ? MANAGED_BY_URL_INVALID_TEXT : `Link: ${linkInfo.url}\nmanaged-by-url: ${managedByURL || 'none'}`}>
<i className='fa fa-external-link-alt' />
</button>
</div>

View File

@@ -1,12 +1,13 @@
import {Tooltip} from 'argo-ui';
import {NotificationType, Tooltip} from 'argo-ui';
import * as React from 'react';
import {ContextApis, AuthSettingsCtx} from '../../../shared/context';
import * as models from '../../../shared/models';
import * as AppUtils from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus} from '../utils';
import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus, MANAGED_BY_URL_INVALID_TEXT, MANAGED_BY_URL_INVALID_TOOLTIP} from '../utils';
import {services} from '../../../shared/services';
import {ViewPreferences} from '../../../shared/services';
import {ResourceIcon} from '../resource-icon';
import {isValidManagedByURL} from '../../../shared/utils';
export interface AppSetTileProps {
appSet: models.ApplicationSet;
@@ -22,6 +23,8 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
const linkInfo = getApplicationLinkURL(appSet, ctx.baseHref);
const healthStatus = getAppSetHealthStatus(appSet);
const managedByURL = getManagedByURL(appSet);
const managedByURLInvalid = !!managedByURL && !isValidManagedByURL(managedByURL);
const handleFavoriteToggle = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -35,6 +38,18 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
const handleExternalLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (managedByURLInvalid) {
ctx.notifications.show({
content: (
<div>
<div style={{fontWeight: 600}}>{MANAGED_BY_URL_INVALID_TEXT}</div>
<div style={{marginTop: 6}}>{MANAGED_BY_URL_INVALID_TOOLTIP}</div>
</div>
),
type: NotificationType.Warning
});
return;
}
if (linkInfo.isExternal) {
window.open(linkInfo.url, '_blank', 'noopener,noreferrer');
} else {
@@ -58,9 +73,20 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro
</div>
<div className='columns small-1'>
<div className='applications-list__external-link'>
<button onClick={handleExternalLinkClick} title={getManagedByURL(appSet) ? `Managed by: ${getManagedByURL(appSet)}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
{managedByURLInvalid ? (
<button
type='button'
className='managed-by-url-invalid'
onClick={handleExternalLinkClick}
style={{cursor: 'not-allowed'}}
title={MANAGED_BY_URL_INVALID_TEXT}>
<i className='fa fa-external-link-alt' />
</button>
) : (
<button type='button' onClick={handleExternalLinkClick} title={managedByURL ? `Managed by: ${managedByURL}` : 'Open application'}>
<i className='fa fa-external-link-alt' />
</button>
)}
<button
title={favList?.includes(appSet.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}
className='large-text-height'

View File

@@ -8,7 +8,7 @@ import * as moment from 'moment';
import {BehaviorSubject, combineLatest, concat, from, fromEvent, Observable, Observer, Subscription} from 'rxjs';
import {debounceTime, map} from 'rxjs/operators';
import {AppContext, Context, ContextApis} from '../../shared/context';
import {isValidURL} from '../../shared/utils';
import {isValidManagedByURL} from '../../shared/utils';
import {ResourceTreeNode} from './application-resource-tree/application-resource-tree';
import {CheckboxField, COLORS, ErrorNotification, Revision} from '../../shared/components';
@@ -18,6 +18,14 @@ import {ApplicationSource} from '../../shared/models';
require('./utils.scss');
export {
MANAGED_BY_URL_INVALID_COLOR,
MANAGED_BY_URL_INVALID_TEXT,
MANAGED_BY_URL_INVALID_TOOLTIP,
managedByURLInvalidLabelStyle,
managedByURLInvalidLabelStyleCompact
} from '../../shared/utils';
export interface NodeId {
kind: string;
namespace: string;
@@ -1990,7 +1998,7 @@ export function getApplicationLinkURL(app: any, baseHref: string, node?: any): {
let url, isExternal;
if (managedByURL) {
// Validate the managed-by URL using the same validation as external links
if (!isValidURL(managedByURL)) {
if (!isValidManagedByURL(managedByURL)) {
// If URL is invalid, fall back to local URL for security
console.warn(`Invalid managed-by URL for application ${app.metadata.name}: ${managedByURL}`);
url = baseHref + 'applications/' + app.metadata.namespace + '/' + app.metadata.name;
@@ -2018,7 +2026,7 @@ export function getApplicationLinkURLFromNode(node: any, baseHref: string): {url
let url, isExternal;
if (managedByURL) {
// Validate the managed-by URL using the same validation as external links
if (!isValidURL(managedByURL)) {
if (!isValidManagedByURL(managedByURL)) {
// If URL is invalid, fall back to local URL for security
console.warn(`Invalid managed-by URL for application ${node.name}: ${managedByURL}`);
url = baseHref + 'applications/' + node.namespace + '/' + node.name;

View File

@@ -1,4 +1,9 @@
/* eslint-env jest */
declare const test: any;
declare const expect: any;
declare const describe: any;
import {concatMaps} from './utils';
import {isValidManagedByURL} from './utils';
test('map concatenation', () => {
const map1 = {
@@ -12,3 +17,24 @@ test('map concatenation', () => {
const map3 = concatMaps(map1, map2);
expect(map3).toEqual(new Map(Object.entries({a: '9', b: '2', c: '8'})));
});
describe('isValidManagedByURL', () => {
test('accepts http/https URLs', () => {
expect(isValidManagedByURL('http://example.com')).toBe(true);
expect(isValidManagedByURL('https://example.com')).toBe(true);
expect(isValidManagedByURL('https://localhost:8081')).toBe(true);
});
test('rejects non-http(s) protocols', () => {
expect(isValidManagedByURL('ftp://localhost:8081')).toBe(false);
expect(isValidManagedByURL('file:///etc/passwd')).toBe(false);
expect(isValidManagedByURL('javascript:alert(1)')).toBe(false);
expect(isValidManagedByURL('data:text/html,<script>alert(1)</script>')).toBe(false);
expect(isValidManagedByURL('vbscript:msgbox(1)')).toBe(false);
});
test('rejects invalid URL strings', () => {
expect(isValidManagedByURL('not-a-url')).toBe(false);
expect(isValidManagedByURL('')).toBe(false);
});
});

View File

@@ -1,4 +1,5 @@
import React from 'react';
import {useEffect, useState} from 'react';
import type {CSSProperties} from 'react';
import {Cluster} from './models';
export function hashCode(str: string) {
@@ -38,6 +39,38 @@ export function isValidURL(url: string): boolean {
}
}
// managed-by-url is expected to mostly if not always point to another Argo CD instance URL,
// so we only consider http/https valid for click-through behavior.
export function isValidManagedByURL(url: string): boolean {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
} catch (err) {
return false;
}
}
export const MANAGED_BY_URL_INVALID_TEXT = 'managed-by-url: invalid url provided';
export const MANAGED_BY_URL_INVALID_TOOLTIP = 'managed-by-url must be a valid http(s) URL for the managing Argo CD instance. The external link is disabled until this is fixed.';
export const MANAGED_BY_URL_INVALID_COLOR = '#f4c030';
export const managedByURLInvalidLabelStyle: CSSProperties = {
color: MANAGED_BY_URL_INVALID_COLOR,
marginLeft: '0.5em',
fontSize: '13px',
fontWeight: 500,
lineHeight: 1.35,
whiteSpace: 'nowrap'
};
export const managedByURLInvalidLabelStyleCompact: CSSProperties = {
...managedByURLInvalidLabelStyle,
marginLeft: '4px',
fontSize: '12px',
fontWeight: 600
};
export const colorSchemes = {
light: '(prefers-color-scheme: light)',
dark: '(prefers-color-scheme: dark)'
@@ -81,9 +114,9 @@ export const useSystemTheme = (cb: (theme: string) => void) => {
};
export const useTheme = (props: {theme: string}) => {
const [theme, setTheme] = React.useState(getTheme(props.theme));
const [theme, setTheme] = useState(getTheme(props.theme));
React.useEffect(() => {
useEffect(() => {
let destroyListener: (() => void) | undefined;
// change theme by system, only register listener when theme is auto