mirror of https://github.com/portainer/portainer
feat(kube): add a11y props for smoke tests [EE-6747] (#11263)
parent
42c2a52a6b
commit
6c70049ecc
|
@ -21,6 +21,7 @@
|
||||||
auto-focus
|
auto-focus
|
||||||
ng-model-options="{ debounce: 300 }"
|
ng-model-options="{ debounce: 300 }"
|
||||||
data-cy="k8sNamespace-namespaceSearchInput"
|
data-cy="k8sNamespace-namespaceSearchInput"
|
||||||
|
aria-label="Search input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar !mr-0 !gap-3" ng-if="$ctrl.isAdmin">
|
<div class="actionBar !mr-0 !gap-3" ng-if="$ctrl.isAdmin">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="datatable">
|
<section class="datatable" aria-label="Helm charts">
|
||||||
<div class="toolBar vertical-center relative w-full flex-wrap !gap-x-5 !gap-y-1 !px-0">
|
<div class="toolBar vertical-center relative w-full flex-wrap !gap-x-5 !gap-y-1 !px-0">
|
||||||
<div class="toolBarTitle vertical-center">
|
<div class="toolBarTitle vertical-center">
|
||||||
{{ $ctrl.titleText }}
|
{{ $ctrl.titleText }}
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<div class="searchBar vertical-center !mr-0">
|
<div class="searchBar vertical-center !mr-0">
|
||||||
<pr-icon icon="'search'" class="searchIcon"></pr-icon>
|
<pr-icon icon="'search'" class="searchIcon"></pr-icon>
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }" />
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }" aria-label="Search input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5">
|
<div class="w-1/5">
|
||||||
<por-select
|
<por-select
|
||||||
|
@ -45,4 +45,4 @@
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!$ctrl.loading && $ctrl.charts.length === 0" class="text-muted text-center"> No helm charts available. </div>
|
<div ng-if="!$ctrl.loading && $ctrl.charts.length === 0" class="text-muted text-center"> No helm charts available. </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<pr-icon icon="'plus'" class="vertical-center"></pr-icon>
|
<pr-icon icon="'plus'" class="vertical-center"></pr-icon>
|
||||||
Show custom values
|
Show custom values
|
||||||
</button>
|
</button>
|
||||||
<span class="small interactive vertical-center" ng-if="$ctrl.state.loadingValues">
|
<span class="small interactive vertical-center" ng-if="$ctrl.state.loadingValues" role="status">
|
||||||
<pr-icon icon="'refresh-cw'" class="mr-1"></pr-icon>
|
<pr-icon icon="'refresh-cw'" class="mr-1"></pr-icon>
|
||||||
Loading values.yaml...
|
Loading values.yaml...
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
id="configuration_name"
|
||||||
name="configuration_name"
|
name="configuration_name"
|
||||||
ng-model="ctrl.formValues.Name"
|
ng-model="ctrl.formValues.Name"
|
||||||
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
|
ng-pattern="/^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/"
|
||||||
|
|
|
@ -197,6 +197,7 @@ export const ngModule = angular
|
||||||
'onChange',
|
'onChange',
|
||||||
'visibleTooltip',
|
'visibleTooltip',
|
||||||
'dataCy',
|
'dataCy',
|
||||||
|
'disabled',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const switchField = r2a(SwitchField, [
|
||||||
'name',
|
'name',
|
||||||
'labelClass',
|
'labelClass',
|
||||||
'fieldClass',
|
'fieldClass',
|
||||||
'dataCy',
|
'data-cy',
|
||||||
'disabled',
|
'disabled',
|
||||||
'onChange',
|
'onChange',
|
||||||
'featureId',
|
'featureId',
|
||||||
|
|
|
@ -43,6 +43,7 @@ export function PageHeader({
|
||||||
onClick={onClickedRefresh}
|
onClick={onClickedRefresh}
|
||||||
className="m-0 p-0 focus:text-inherit"
|
className="m-0 p-0 focus:text-inherit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
title="Refresh page"
|
||||||
>
|
>
|
||||||
<RefreshCw className="icon" />
|
<RefreshCw className="icon" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export function TextTip({
|
||||||
'small gap-1 align-top text-xs',
|
'small gap-1 align-top text-xs',
|
||||||
inline ? 'inline-flex' : 'flex'
|
inline ? 'inline-flex' : 'flex'
|
||||||
)}
|
)}
|
||||||
|
role="status"
|
||||||
>
|
>
|
||||||
<Icon icon={icon} mode={getMode(color)} className="!mt-0.5 flex-none" />
|
<Icon icon={icon} mode={getMode(color)} className="!mt-0.5 flex-none" />
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ export function Datatable<D extends DefaultType>({
|
||||||
const selectedItems = selectedRowModel.rows.map((row) => row.original);
|
const selectedItems = selectedRowModel.rows.map((row) => row.original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Container noWidget={noWidget}>
|
<Table.Container noWidget={noWidget} aria-label={title}>
|
||||||
<DatatableHeader
|
<DatatableHeader
|
||||||
onSearchChange={handleSearchBarChange}
|
onSearchChange={handleSearchBarChange}
|
||||||
searchValue={settings.search}
|
searchValue={settings.search}
|
||||||
|
|
|
@ -5,24 +5,30 @@ import { Widget, WidgetBody } from '@@/Widget';
|
||||||
interface Props {
|
interface Props {
|
||||||
// workaround to remove the widget, ideally we should have a different component to wrap the table with a widget
|
// workaround to remove the widget, ideally we should have a different component to wrap the table with a widget
|
||||||
noWidget?: boolean;
|
noWidget?: boolean;
|
||||||
|
'aria-label'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableContainer({
|
export function TableContainer({
|
||||||
children,
|
children,
|
||||||
noWidget = false,
|
noWidget = false,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
if (noWidget) {
|
if (noWidget) {
|
||||||
return <div className="datatable">{children}</div>;
|
return (
|
||||||
|
<section className="datatable" aria-label={ariaLabel}>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<div className="datatable">
|
<section className="datatable" aria-label={ariaLabel}>
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetBody className="no-padding">{children}</WidgetBody>
|
<WidgetBody className="no-padding">{children}</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface Props {
|
||||||
dataCy?: string;
|
dataCy?: string;
|
||||||
// true if you want to always show the tooltip
|
// true if you want to always show the tooltip
|
||||||
visibleTooltip?: boolean;
|
visibleTooltip?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Slider({
|
export function Slider({
|
||||||
|
@ -27,6 +28,7 @@ export function Slider({
|
||||||
onChange,
|
onChange,
|
||||||
dataCy,
|
dataCy,
|
||||||
visibleTooltip: visible,
|
visibleTooltip: visible,
|
||||||
|
disabled,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const marks = {
|
const marks = {
|
||||||
[min]: visible && value / max < 0.1 ? '' : translateMinValue(min),
|
[min]: visible && value / max < 0.1 ? '' : translateMinValue(min),
|
||||||
|
@ -34,14 +36,14 @@ export function Slider({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root} data-cy={dataCy}>
|
||||||
<RcSlider
|
<RcSlider
|
||||||
handleRender={visible ? sliderTooltip : undefined}
|
handleRender={visible ? sliderTooltip : undefined}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
step={step}
|
step={step}
|
||||||
data-cy={dataCy}
|
disabled={disabled}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,6 +10,7 @@ export function SliderWithInput({
|
||||||
step = 1,
|
step = 1,
|
||||||
dataCy,
|
dataCy,
|
||||||
visibleTooltip = false,
|
visibleTooltip = false,
|
||||||
|
inputId,
|
||||||
}: {
|
}: {
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
|
@ -18,6 +19,7 @@ export function SliderWithInput({
|
||||||
dataCy: string;
|
dataCy: string;
|
||||||
step?: number;
|
step?: number;
|
||||||
visibleTooltip?: boolean;
|
visibleTooltip?: boolean;
|
||||||
|
inputId?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
|
@ -44,6 +46,7 @@ export function SliderWithInput({
|
||||||
onChange={(e) => onChange(e.target.valueAsNumber)}
|
onChange={(e) => onChange(e.target.valueAsNumber)}
|
||||||
className="w-32"
|
className="w-32"
|
||||||
data-cy={`${dataCy}Input`}
|
data-cy={`${dataCy}Input`}
|
||||||
|
id={inputId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
|
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@ import './Switch.css';
|
||||||
|
|
||||||
import styles from './Switch.module.css';
|
import styles from './Switch.module.css';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props extends AutomationTestingProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -17,7 +18,6 @@ export interface Props {
|
||||||
|
|
||||||
index?: number;
|
index?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
dataCy?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
featureId?: FeatureId;
|
featureId?: FeatureId;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ export function Switch({
|
||||||
checked,
|
checked,
|
||||||
id,
|
id,
|
||||||
disabled,
|
disabled,
|
||||||
dataCy,
|
'data-cy': dataCy,
|
||||||
onChange,
|
onChange,
|
||||||
index,
|
index,
|
||||||
featureId,
|
featureId,
|
||||||
|
@ -42,6 +42,8 @@ export function Switch({
|
||||||
business: limitedToBE,
|
business: limitedToBE,
|
||||||
limited: limitedToBE,
|
limited: limitedToBE,
|
||||||
})}
|
})}
|
||||||
|
data-cy={dataCy}
|
||||||
|
aria-checked={checked}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -51,7 +53,7 @@ export function Switch({
|
||||||
disabled={disabled || limitedToBE}
|
disabled={disabled || limitedToBE}
|
||||||
onChange={({ target: { checked } }) => onChange(checked, index)}
|
onChange={({ target: { checked } }) => onChange(checked, index)}
|
||||||
/>
|
/>
|
||||||
<span className="slider round before:content-['']" data-cy={dataCy} />
|
<span className="slider round before:content-['']" />
|
||||||
</label>
|
</label>
|
||||||
{limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
{limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -3,13 +3,14 @@ import uuid from 'uuid';
|
||||||
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
|
||||||
import styles from './SwitchField.module.css';
|
import styles from './SwitchField.module.css';
|
||||||
import { Switch } from './Switch';
|
import { Switch } from './Switch';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props extends AutomationTestingProps {
|
||||||
label: string;
|
label: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange(value: boolean, index?: number): void;
|
onChange(value: boolean, index?: number): void;
|
||||||
|
@ -21,7 +22,7 @@ export interface Props {
|
||||||
labelClass?: string;
|
labelClass?: string;
|
||||||
switchClass?: string;
|
switchClass?: string;
|
||||||
fieldClass?: string;
|
fieldClass?: string;
|
||||||
dataCy?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
featureId?: FeatureId;
|
featureId?: FeatureId;
|
||||||
valueExplanation?: ReactNode;
|
valueExplanation?: ReactNode;
|
||||||
|
@ -35,7 +36,7 @@ export function SwitchField({
|
||||||
name = uuid(),
|
name = uuid(),
|
||||||
labelClass,
|
labelClass,
|
||||||
fieldClass,
|
fieldClass,
|
||||||
dataCy,
|
'data-cy': dataCy,
|
||||||
disabled,
|
disabled,
|
||||||
onChange,
|
onChange,
|
||||||
featureId,
|
featureId,
|
||||||
|
@ -65,7 +66,7 @@ export function SwitchField({
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
index={index}
|
index={index}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
dataCy={dataCy}
|
data-cy={dataCy}
|
||||||
/>
|
/>
|
||||||
{valueExplanation && <span>{valueExplanation}</span>}
|
{valueExplanation && <span>{valueExplanation}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function StorageClassDatatable({ storageClassValues }: Props) {
|
||||||
className="mr-2 mb-0"
|
className="mr-2 mb-0"
|
||||||
id={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
id={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
||||||
name={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
name={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
||||||
dataCy={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
data-cy={`kubeSetup-storageToggle${storageClassValue.Name}`}
|
||||||
/>
|
/>
|
||||||
<span>{storageClassValue.Name}</span>
|
<span>{storageClassValue.Name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +69,7 @@ export function StorageClassDatatable({ storageClassValues }: Props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="mr-2 mb-0"
|
className="mr-2 mb-0"
|
||||||
dataCy={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
data-cy={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
||||||
id={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
id={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
||||||
name={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
name={`kubeSetup-storageExpansionToggle${storageClassValue.Name}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,10 +11,10 @@ import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { Card } from '@@/Card';
|
|
||||||
import { InputGroup } from '@@/form-components/InputGroup';
|
|
||||||
import { InlineLoader } from '@@/InlineLoader';
|
import { InlineLoader } from '@@/InlineLoader';
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
|
import { Card } from '@@/Card';
|
||||||
|
import { InputGroup } from '@@/form-components/InputGroup';
|
||||||
|
|
||||||
import { AnnotationsForm } from '../../annotations/AnnotationsForm';
|
import { AnnotationsForm } from '../../annotations/AnnotationsForm';
|
||||||
|
|
||||||
|
@ -399,9 +399,16 @@ export function IngressForm({
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="form-group col-sm-6 col-lg-4 !pl-0 !pr-2">
|
<div className="form-group col-sm-6 col-lg-4 !pl-0 !pr-2">
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon required>Hostname</InputGroup.Addon>
|
<InputGroup.Addon
|
||||||
|
required
|
||||||
|
as="label"
|
||||||
|
htmlFor={`ingress_host_${hostIndex}`}
|
||||||
|
>
|
||||||
|
Hostname
|
||||||
|
</InputGroup.Addon>
|
||||||
<InputGroup.Input
|
<InputGroup.Input
|
||||||
name={`ingress_host_${hostIndex}`}
|
name={`ingress_host_${hostIndex}`}
|
||||||
|
id={`ingress_host_${hostIndex}`}
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="e.g. example.com"
|
placeholder="e.g. example.com"
|
||||||
|
@ -411,6 +418,7 @@ export function IngressForm({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{errors[`hosts[${hostIndex}].host`] && (
|
{errors[`hosts[${hostIndex}].host`] && (
|
||||||
<FormError className="!mb-0 mt-1">
|
<FormError className="!mb-0 mt-1">
|
||||||
{errors[`hosts[${hostIndex}].host`]}
|
{errors[`hosts[${hostIndex}].host`]}
|
||||||
|
@ -420,10 +428,17 @@ export function IngressForm({
|
||||||
|
|
||||||
<div className="form-group col-sm-6 col-lg-4 !pl-2 !pr-0">
|
<div className="form-group col-sm-6 col-lg-4 !pl-2 !pr-0">
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon>TLS secret</InputGroup.Addon>
|
<InputGroup.Addon
|
||||||
|
as="label"
|
||||||
|
htmlFor={`ingress_tls_${hostIndex}`}
|
||||||
|
>
|
||||||
|
TLS secret
|
||||||
|
</InputGroup.Addon>
|
||||||
<Select
|
<Select
|
||||||
key={tlsOptions.toString() + host.Secret}
|
key={tlsOptions.toString() + host.Secret}
|
||||||
name={`ingress_tls_${hostIndex}`}
|
name={`ingress_tls_${hostIndex}`}
|
||||||
|
inputId={`ingress_tls_${hostIndex}`}
|
||||||
|
options={tlsOptions}
|
||||||
value={
|
value={
|
||||||
host.Secret !== undefined
|
host.Secret !== undefined
|
||||||
? {
|
? {
|
||||||
|
@ -442,7 +457,6 @@ export function IngressForm({
|
||||||
}
|
}
|
||||||
noOptionsMessage={() => 'No TLS secrets available'}
|
noOptionsMessage={() => 'No TLS secrets available'}
|
||||||
size="sm"
|
size="sm"
|
||||||
options={tlsOptions}
|
|
||||||
/>
|
/>
|
||||||
{!host.NoHost && (
|
{!host.NoHost && (
|
||||||
<div className="input-group-btn">
|
<div className="input-group-btn">
|
||||||
|
@ -501,10 +515,17 @@ export function IngressForm({
|
||||||
>
|
>
|
||||||
<div className="form-group col-sm-3 col-xl-2 !m-0 !pl-0">
|
<div className="form-group col-sm-3 col-xl-2 !m-0 !pl-0">
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon required>Service</InputGroup.Addon>
|
<InputGroup.Addon
|
||||||
|
required
|
||||||
|
as="label"
|
||||||
|
htmlFor={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||||
|
>
|
||||||
|
Service
|
||||||
|
</InputGroup.Addon>
|
||||||
<Select
|
<Select
|
||||||
key={serviceOptions.toString() + path.ServiceName}
|
key={serviceOptions.toString() + path.ServiceName}
|
||||||
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||||
|
id={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||||
options={serviceOptions}
|
options={serviceOptions}
|
||||||
value={
|
value={
|
||||||
path.ServiceName
|
path.ServiceName
|
||||||
|
@ -551,12 +572,17 @@ export function IngressForm({
|
||||||
{servicePorts && (
|
{servicePorts && (
|
||||||
<>
|
<>
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon required>
|
<InputGroup.Addon
|
||||||
|
required
|
||||||
|
as="label"
|
||||||
|
htmlFor={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
||||||
|
>
|
||||||
Service port
|
Service port
|
||||||
</InputGroup.Addon>
|
</InputGroup.Addon>
|
||||||
<Select
|
<Select
|
||||||
key={servicePorts.toString() + path.ServicePort}
|
key={servicePorts.toString() + path.ServicePort}
|
||||||
name={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
name={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
||||||
|
id={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
||||||
options={
|
options={
|
||||||
servicePorts[path.ServiceName]?.map(
|
servicePorts[path.ServiceName]?.map(
|
||||||
(portOption) => ({
|
(portOption) => ({
|
||||||
|
@ -607,10 +633,16 @@ export function IngressForm({
|
||||||
|
|
||||||
<div className="form-group col-sm-3 col-xl-2 !m-0 !pl-0">
|
<div className="form-group col-sm-3 col-xl-2 !m-0 !pl-0">
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon>Path type</InputGroup.Addon>
|
<InputGroup.Addon
|
||||||
<Select<Option<string>>
|
as="label"
|
||||||
|
htmlFor={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
||||||
|
>
|
||||||
|
Path type
|
||||||
|
</InputGroup.Addon>
|
||||||
|
<Select
|
||||||
key={servicePorts.toString() + path.PathType}
|
key={servicePorts.toString() + path.PathType}
|
||||||
name={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
name={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
||||||
|
id={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
||||||
options={
|
options={
|
||||||
pathTypes?.map((type) => ({
|
pathTypes?.map((type) => ({
|
||||||
label: type,
|
label: type,
|
||||||
|
@ -657,10 +689,17 @@ export function IngressForm({
|
||||||
|
|
||||||
<div className="form-group col-sm-3 col-xl-3 !m-0 !pl-0">
|
<div className="form-group col-sm-3 col-xl-3 !m-0 !pl-0">
|
||||||
<InputGroup size="small">
|
<InputGroup size="small">
|
||||||
<InputGroup.Addon required>Path</InputGroup.Addon>
|
<InputGroup.Addon
|
||||||
|
required
|
||||||
|
as="label"
|
||||||
|
htmlFor={`ingress_route_${hostIndex}-${pathIndex}`}
|
||||||
|
>
|
||||||
|
Path
|
||||||
|
</InputGroup.Addon>
|
||||||
<InputGroup.Input
|
<InputGroup.Input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
name={`ingress_route_${hostIndex}-${pathIndex}`}
|
name={`ingress_route_${hostIndex}-${pathIndex}`}
|
||||||
|
id={`ingress_route_${hostIndex}-${pathIndex}`}
|
||||||
placeholder="/example"
|
placeholder="/example"
|
||||||
data-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
data-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
||||||
data-cy={`k8sAppCreate-route_${hostIndex}-${pathIndex}`}
|
data-cy={`k8sAppCreate-route_${hostIndex}-${pathIndex}`}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function LoadBalancerFormSection() {
|
||||||
disable the use of load balancers in this namespace.
|
disable the use of load balancers in this namespace.
|
||||||
</TextTip>
|
</TextTip>
|
||||||
<SwitchField
|
<SwitchField
|
||||||
dataCy="k8sNamespaceCreate-loadBalancerQuotaToggle"
|
data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
|
||||||
label="Load balancer quota"
|
label="Load balancer quota"
|
||||||
labelClass="col-sm-3 col-lg-2"
|
labelClass="col-sm-3 col-lg-2"
|
||||||
fieldClass="pt-2"
|
fieldClass="pt-2"
|
||||||
|
|
|
@ -81,16 +81,19 @@ export function ResourceQuotaFormSection({
|
||||||
inputId="memory-limit"
|
inputId="memory-limit"
|
||||||
>
|
>
|
||||||
<div className="col-xs-8">
|
<div className="col-xs-8">
|
||||||
<SliderWithInput
|
{memoryLimit >= 0 && (
|
||||||
value={Number(values.memory) ?? 0}
|
<SliderWithInput
|
||||||
onChange={(value) =>
|
value={Number(values.memory) ?? 0}
|
||||||
onChange({ ...values, memory: `${value}` })
|
onChange={(value) =>
|
||||||
}
|
onChange({ ...values, memory: `${value}` })
|
||||||
max={memoryLimit}
|
}
|
||||||
step={128}
|
max={memoryLimit}
|
||||||
dataCy="k8sNamespaceCreate-memoryLimit"
|
step={128}
|
||||||
visibleTooltip
|
dataCy="k8sNamespaceCreate-memoryLimit"
|
||||||
/>
|
visibleTooltip
|
||||||
|
inputId="memory-limit"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{errors?.memory && (
|
{errors?.memory && (
|
||||||
<FormError className="pt-1">{errors.memory}</FormError>
|
<FormError className="pt-1">{errors.memory}</FormError>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function AccessControlForm({
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<SwitchField
|
<SwitchField
|
||||||
dataCy="portainer-accessMgmtToggle"
|
data-cy="portainer-accessMgmtToggle"
|
||||||
checked={accessControlEnabled}
|
checked={accessControlEnabled}
|
||||||
name={withNamespace('accessControlEnabled')}
|
name={withNamespace('accessControlEnabled')}
|
||||||
label="Enable access control"
|
label="Enable access control"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { pluralize } from '@/portainer/helpers/strings';
|
import { pluralize } from '@/portainer/helpers/strings';
|
||||||
|
|
||||||
import { confirmDestructive } from '@@/modals/confirm';
|
import { confirmDestructive } from '@@/modals/confirm';
|
||||||
import { Button } from '@@/buttons';
|
import { AddButton, Button } from '@@/buttons';
|
||||||
|
|
||||||
import { HelmRepository } from './types';
|
import { HelmRepository } from './types';
|
||||||
import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service';
|
import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service';
|
||||||
|
@ -18,7 +18,7 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
|
||||||
const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation();
|
const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={selectedItems.length < 1}
|
disabled={selectedItems.length < 1}
|
||||||
color="dangerlight"
|
color="dangerlight"
|
||||||
|
@ -29,16 +29,10 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<AddButton to="portainer.account.createHelmRepository">
|
||||||
onClick={() =>
|
|
||||||
router.stateService.go('portainer.account.createHelmRepository')
|
|
||||||
}
|
|
||||||
data-cy="credentials-addButton"
|
|
||||||
icon={Plus}
|
|
||||||
>
|
|
||||||
Add Helm repository
|
Add Helm repository
|
||||||
</Button>
|
</AddButton>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onDeleteClick(selectedItems: HelmRepository[]) {
|
async function onDeleteClick(selectedItems: HelmRepository[]) {
|
||||||
|
|
|
@ -39,13 +39,18 @@ export function HelmRepositoryForm({
|
||||||
>
|
>
|
||||||
{({ values, errors, handleSubmit, isValid, dirty }) => (
|
{({ values, errors, handleSubmit, isValid, dirty }) => (
|
||||||
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
|
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
|
||||||
<FormControl inputId="url" label="URL" errors={errors.URL} required>
|
<FormControl
|
||||||
|
inputId="url-field"
|
||||||
|
label="URL"
|
||||||
|
errors={errors.URL}
|
||||||
|
required
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
as={Input}
|
as={Input}
|
||||||
name="URL"
|
name="URL"
|
||||||
value={values.URL}
|
value={values.URL}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
id="URL"
|
id="url-field"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|
|
@ -81,6 +81,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
to="docker.templates"
|
to="docker.templates"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
data-cy="portainerSidebar-templates"
|
data-cy="portainerSidebar-templates"
|
||||||
|
listId="dockerSidebar-templates"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Application"
|
label="Application"
|
||||||
|
@ -185,6 +186,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
to={setupSubMenuProps.to}
|
to={setupSubMenuProps.to}
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
data-cy="portainerSidebar-host-area"
|
data-cy="portainerSidebar-host-area"
|
||||||
|
listId="portainerSidebar-host-area"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Details"
|
label="Details"
|
||||||
|
|
|
@ -58,6 +58,7 @@ export function EdgeComputeSidebar() {
|
||||||
label="Edge Templates"
|
label="Edge Templates"
|
||||||
to="edge.templates"
|
to="edge.templates"
|
||||||
data-cy="edgeSidebar-templates"
|
data-cy="edgeSidebar-templates"
|
||||||
|
listId="edgeSidebar-templates"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Application"
|
label="Application"
|
||||||
|
|
|
@ -59,6 +59,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
pathOptions={{ includePaths: ['kubernetes.ingresses'] }}
|
pathOptions={{ includePaths: ['kubernetes.ingresses'] }}
|
||||||
data-cy="k8sSidebar-networking"
|
data-cy="k8sSidebar-networking"
|
||||||
|
listId="k8sSidebar-networking"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.services"
|
to="kubernetes.services"
|
||||||
|
@ -97,7 +98,8 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
to="kubernetes.cluster"
|
to="kubernetes.cluster"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
pathOptions={{ includePaths: ['kubernetes.registries'] }}
|
pathOptions={{ includePaths: ['kubernetes.registries'] }}
|
||||||
data-cy="k8sSidebar-cluster"
|
data-cy="k8sSidebar-cluster-area"
|
||||||
|
listId="k8sSidebar-cluster-area"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Details"
|
label="Details"
|
||||||
|
@ -108,7 +110,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
]}
|
]}
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
isSubMenu
|
isSubMenu
|
||||||
data-cy="k8sSidebar-clusterDetails"
|
data-cy="k8sSidebar-cluster"
|
||||||
/>
|
/>
|
||||||
<Authorized
|
<Authorized
|
||||||
authorizations="K8sClusterSetupRW"
|
authorizations="K8sClusterSetupRW"
|
||||||
|
|
|
@ -39,6 +39,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
to="portainer.users"
|
to="portainer.users"
|
||||||
pathOptions={{ includePaths: ['portainer.teams', 'portainer.roles'] }}
|
pathOptions={{ includePaths: ['portainer.teams', 'portainer.roles'] }}
|
||||||
data-cy="portainerSidebar-userRelated"
|
data-cy="portainerSidebar-userRelated"
|
||||||
|
listId="portainerSidebar-userRelated"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.users"
|
to="portainer.users"
|
||||||
|
@ -77,6 +78,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
data-cy="portainerSidebar-environments-area"
|
data-cy="portainerSidebar-environments-area"
|
||||||
|
listId="portainer-environments"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Environments"
|
label="Environments"
|
||||||
|
@ -125,6 +127,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
includePaths: ['portainer.activityLogs'],
|
includePaths: ['portainer.activityLogs'],
|
||||||
}}
|
}}
|
||||||
data-cy="k8sSidebar-logs"
|
data-cy="k8sSidebar-logs"
|
||||||
|
listId="k8sSidebar-logs"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Authentication"
|
label="Authentication"
|
||||||
|
@ -147,6 +150,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
icon={HardDrive}
|
icon={HardDrive}
|
||||||
to="portainer.endpoints.updateSchedules"
|
to="portainer.endpoints.updateSchedules"
|
||||||
data-cy="portainerSidebar-environments-area"
|
data-cy="portainerSidebar-environments-area"
|
||||||
|
listId="portainer-environments-area"
|
||||||
>
|
>
|
||||||
<EdgeUpdatesSidebarItem />
|
<EdgeUpdatesSidebarItem />
|
||||||
</SidebarParent>
|
</SidebarParent>
|
||||||
|
@ -164,6 +168,7 @@ export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
label="Settings"
|
label="Settings"
|
||||||
icon={Settings}
|
icon={Settings}
|
||||||
data-cy="portainerSidebar-settings"
|
data-cy="portainerSidebar-settings"
|
||||||
|
listId="portainer-settings"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.settings"
|
to="portainer.settings"
|
||||||
|
|
|
@ -2,6 +2,8 @@ import clsx from 'clsx';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
@ -15,9 +17,9 @@ type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
to: string;
|
to: string;
|
||||||
'data-cy': string;
|
|
||||||
pathOptions?: PathOptions;
|
pathOptions?: PathOptions;
|
||||||
params?: object;
|
params?: object;
|
||||||
|
listId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SidebarParent({
|
export function SidebarParent({
|
||||||
|
@ -27,8 +29,9 @@ export function SidebarParent({
|
||||||
to,
|
to,
|
||||||
params,
|
params,
|
||||||
pathOptions,
|
pathOptions,
|
||||||
|
listId,
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props & AutomationTestingProps>) {
|
||||||
const anchorProps = useSidebarSrefActive(
|
const anchorProps = useSidebarSrefActive(
|
||||||
to,
|
to,
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -78,7 +81,14 @@ export function SidebarParent({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
|
className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsExpanded((isExpanded) => !isExpanded);
|
||||||
|
}}
|
||||||
|
title={isExpanded ? 'Collapse' : 'Expand'}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={listId}
|
||||||
>
|
>
|
||||||
<div className="flex items-center group-hover:bg-blue-5 be:group-hover:bg-gray-5 group-hover:th-dark:bg-gray-true-7 group-hover:bg-opacity-10 be:group-hover:bg-opacity-10 rounded-full p-[3px] transition ease-in-out">
|
<div className="flex items-center group-hover:bg-blue-5 be:group-hover:bg-gray-5 group-hover:th-dark:bg-gray-true-7 group-hover:bg-opacity-10 be:group-hover:bg-opacity-10 rounded-full p-[3px] transition ease-in-out">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -98,6 +108,7 @@ export function SidebarParent({
|
||||||
|
|
||||||
const childList = (
|
const childList = (
|
||||||
<ul
|
<ul
|
||||||
|
id={listId}
|
||||||
// pl-11 must be important because it needs to avoid the padding from '.root ul' in sidebar.module.css
|
// pl-11 must be important because it needs to avoid the padding from '.root ul' in sidebar.module.css
|
||||||
className={clsx('text-white !pl-11', {
|
className={clsx('text-white !pl-11', {
|
||||||
hidden: !isExpanded,
|
hidden: !isExpanded,
|
||||||
|
|
Loading…
Reference in New Issue