mirror of https://github.com/rancher/dashboard.git
merge master and resolve conflicts
This commit is contained in:
commit
18e7e51614
|
|
@ -60,7 +60,7 @@ jobs:
|
|||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ inputs.registry_target }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import ComponentPo from '@/cypress/e2e/po/components/component.po';
|
||||
import RadioGroupInputPo from '@/cypress/e2e/po/components/radio-group-input.po';
|
||||
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
||||
|
||||
export default class ACE extends ComponentPo {
|
||||
constructor(selector = '.dashboard-root') {
|
||||
super(selector);
|
||||
}
|
||||
|
||||
enable() {
|
||||
const radioButton = new RadioGroupInputPo('[data-testid="ace-enabled-radio-input"]');
|
||||
|
||||
return radioButton.set(1);
|
||||
}
|
||||
|
||||
fqdn() {
|
||||
return LabeledInputPo.byLabel(this.self(), 'FQDN');
|
||||
}
|
||||
|
||||
caCerts() {
|
||||
return LabeledInputPo.byLabel(this.self(), 'CA Certificates');
|
||||
}
|
||||
|
||||
enterFdqn(val: string) {
|
||||
return new LabeledInputPo('[data-testid="ace-fqdn-input"]').set(val);
|
||||
}
|
||||
|
||||
enterCaCerts(val: string) {
|
||||
return new LabeledInputPo('[data-testid="ace-cacerts-input"]').set(val);
|
||||
}
|
||||
}
|
||||
|
|
@ -82,4 +82,8 @@ export default class ComponentPo {
|
|||
checkNotExists(options?: GetOptions): Cypress.Chainable<boolean> {
|
||||
return this.self(options).should('not.exist');
|
||||
}
|
||||
|
||||
shouldHaveValue(value: string, options?: GetOptions): Cypress.Chainable<boolean> {
|
||||
return this.self(options).should('have.value', value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import ComponentPo from '@/cypress/e2e/po/components/component.po';
|
||||
|
||||
export class GrowlManagerPo extends ComponentPo {
|
||||
constructor() {
|
||||
super('.growl-container');
|
||||
}
|
||||
|
||||
growlList() {
|
||||
return this.self().find('.growl-list');
|
||||
}
|
||||
|
||||
growlMessage() {
|
||||
return this.self().find('.growl-message');
|
||||
}
|
||||
|
||||
dismissWarning() {
|
||||
return this.self().find('.icon-close').click();
|
||||
}
|
||||
|
||||
dismissAllWarnings() {
|
||||
return this.self().find('button.btn').contains('Clear All Notifications').click();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import ComponentPo from '@/cypress/e2e/po/components/component.po';
|
||||
|
||||
export default class KeyValuePo extends ComponentPo {
|
||||
addButton(label: string) {
|
||||
return this.self().find('[data-testid="add_row_item_button"]').contains(label);
|
||||
}
|
||||
|
||||
setKeyValueAtIndex(label: string, key: string, value: string, index: number, selector: string) {
|
||||
this.addButton(label).click();
|
||||
this.self().find(`${ selector } [data-testid="input-kv-item-key-${ index }"]`).type(key);
|
||||
this.self().find(`${ selector } [data-testid="kv-item-value-${ index }"]`).type(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,14 @@ export default class LabeledInputPo extends ComponentPo {
|
|||
});
|
||||
}
|
||||
|
||||
expectToBeDisabled(): Cypress.Chainable {
|
||||
return this.self().should('have.attr', 'disabled', 'disabled');
|
||||
}
|
||||
|
||||
expectToBeEnabled(): Cypress.Chainable {
|
||||
return this.self().should('not.have.attr', 'disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the input HTML element from given container
|
||||
* @returns HTML Element
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ export default class TabbedPo extends ComponentPo {
|
|||
return this.self().get('[data-testid="tabbed-block"] > li');
|
||||
}
|
||||
|
||||
assertTabIsActive(selector: string) {
|
||||
return this.self().find(`${ selector }`).should('have.class', 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tab labels
|
||||
* @param tabLabelsSelector
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
import CreateEditViewPo from '@/cypress/e2e/po/components/create-edit-view.po';
|
||||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
|
||||
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
||||
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
|
||||
import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po';
|
||||
import BannersPo from '@/cypress/e2e/po/components/banners.po';
|
||||
import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po';
|
||||
export default class NetworkPolicyPo extends CreateEditViewPo {
|
||||
constructor(selector = '.dashboard-root') {
|
||||
super(selector);
|
||||
import ArrayListPo from '@/cypress/e2e/po/components/array-list.po';
|
||||
|
||||
export default class CreateEditNetworkPolicyPagePo extends PagePo {
|
||||
private static createPath(clusterId: string, namespace?: string, id?: string ) {
|
||||
const root = `/c/${ clusterId }/explorer/networking.k8s.io.networkpolicy`;
|
||||
|
||||
return id ? `${ root }/${ namespace }/${ id }` : `${ root }/create`;
|
||||
}
|
||||
|
||||
static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
|
||||
constructor(clusterId = 'local', namespace?: string, id?: string) {
|
||||
super(CreateEditNetworkPolicyPagePo.createPath(clusterId, namespace, id));
|
||||
}
|
||||
|
||||
nameInput() {
|
||||
|
|
@ -30,6 +42,14 @@ export default class NetworkPolicyPo extends CreateEditViewPo {
|
|||
return cy.get('[data-testid="array-list-button"]').contains('Add allowed traffic source');
|
||||
}
|
||||
|
||||
addAllowedPortButton() {
|
||||
return cy.get('[data-testid="array-list-button"]').contains('Add allowed port');
|
||||
}
|
||||
|
||||
ingressRuleItem(index: number) {
|
||||
return new ArrayListPo('section #rule-ingress0', this.self()).arrayListItem(index);
|
||||
}
|
||||
|
||||
policyRuleTargetSelect(index: number) {
|
||||
return new LabeledSelectPo(`[data-testid="policy-rule-target-${ index }"] [data-testid="policy-rule-target-type-labeled-select"]`, this.self());
|
||||
}
|
||||
|
|
@ -4,14 +4,26 @@ import CruResourcePo from '@/cypress/e2e/po/components/cru-resource.po';
|
|||
import ResourceYamlPo from '@/cypress/e2e/po/components/resource-yaml.po';
|
||||
|
||||
export default class ResourceDetailPo extends ComponentPo {
|
||||
/**
|
||||
* components for handling CRUD operations for resources, including cancel/save buttons
|
||||
* @returns
|
||||
*/
|
||||
cruResource() {
|
||||
return new CruResourcePo(this.self());
|
||||
}
|
||||
|
||||
/**
|
||||
* components for managing the resource creation and edit forms
|
||||
* @returns
|
||||
*/
|
||||
createEditView() {
|
||||
return new CreateEditViewPo(this.self());
|
||||
}
|
||||
|
||||
/**
|
||||
* components for YAML editor
|
||||
* @returns
|
||||
*/
|
||||
resourceYaml() {
|
||||
return new ResourceYamlPo(this.self());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,76 @@
|
|||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||
import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po';
|
||||
import ResourceDetailPo from '@/cypress/e2e/po/edit/resource-detail.po';
|
||||
import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po';
|
||||
import TabbedPo from '@/cypress/e2e/po/components/tabbed.po';
|
||||
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
||||
import ArrayListPo from '@/cypress/e2e/po/components/array-list.po';
|
||||
import KeyValuePo from '@/cypress/e2e/po/components/key-value.po';
|
||||
|
||||
export default class WorkloadsCreateEditPo extends PagePo {
|
||||
private static createPath(clusterId: string, id?: string ) {
|
||||
const root = `/c/${ clusterId }/explorer/storage.k8s.io.storageclass/create`;
|
||||
export default class ServicesCreateEditPo extends PagePo {
|
||||
private static createPath(clusterId: string, namespace?: string, id?: string ) {
|
||||
const root = `/c/${ clusterId }/explorer/service`;
|
||||
|
||||
return id ? `${ root }/${ id }` : `${ root }/create`;
|
||||
return id ? `${ root }/${ namespace }/${ id }` : `${ root }/create`;
|
||||
}
|
||||
|
||||
static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
|
||||
constructor(clusterId = '_', id?: string) {
|
||||
super(WorkloadsCreateEditPo.createPath(clusterId, id));
|
||||
constructor(clusterId = 'local', namespace?: string, id?: string) {
|
||||
super(ServicesCreateEditPo.createPath(clusterId, namespace, id));
|
||||
}
|
||||
|
||||
resourceDetail() {
|
||||
return new ResourceDetailPo(this.self());
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.self().get('.title .primaryheader h1');
|
||||
}
|
||||
|
||||
nameNsDescription() {
|
||||
return new NameNsDescription(this.self());
|
||||
}
|
||||
|
||||
selectNamespace(label: string) {
|
||||
const selectNs = new LabeledSelectPo(`[data-testid="name-ns-description-namespace"]`, this.self());
|
||||
|
||||
selectNs.toggle();
|
||||
selectNs.clickLabel(label);
|
||||
}
|
||||
|
||||
selectServiceOption(index: number) {
|
||||
return this.resourceDetail().cruResource().selectSubTypeByIndex(index).click();
|
||||
}
|
||||
|
||||
tabs() {
|
||||
return new TabbedPo('[data-testid="tabbed"]');
|
||||
}
|
||||
|
||||
externalNameTab() {
|
||||
return this.tabs().clickTabWithSelector('[data-testid="define-external-name"]');
|
||||
}
|
||||
|
||||
externalNameInput() {
|
||||
return new LabeledInputPo('#define-external-name .labeled-input input');
|
||||
}
|
||||
|
||||
ipAddressesTab() {
|
||||
return this.tabs().clickTabWithSelector('[data-testid="ips"]');
|
||||
}
|
||||
|
||||
ipAddressList() {
|
||||
return new ArrayListPo('section#ips');
|
||||
}
|
||||
|
||||
lablesAnnotationsTab() {
|
||||
return this.tabs().clickTabWithSelector('[data-testid="btn-labels-and-annotations"]');
|
||||
}
|
||||
|
||||
lablesAnnotationsKeyValue() {
|
||||
return new KeyValuePo('section#labels-and-annotations');
|
||||
}
|
||||
|
||||
errorBanner() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
||||
import ACE from '@/cypress/e2e/po/components/ace.po';
|
||||
import ResourceDetailPo from '@/cypress/e2e/po/edit/resource-detail.po';
|
||||
|
||||
/**
|
||||
* Edit page for imported cluster
|
||||
*/
|
||||
export default class ClusterManagerEditImportedPagePo extends PagePo {
|
||||
private static createPath(clusterId: string, clusterName: string) {
|
||||
return `/c/${ clusterId }/manager/provisioning.cattle.io.cluster/fleet-default/${ clusterName }`;
|
||||
}
|
||||
|
||||
static goTo(clusterId: string, clusterName: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
return super.goTo(ClusterManagerEditImportedPagePo.createPath(clusterId, clusterName));
|
||||
}
|
||||
|
||||
constructor(clusterId = '_', clusterName: string) {
|
||||
super(ClusterManagerEditImportedPagePo.createPath(clusterId, clusterName));
|
||||
}
|
||||
|
||||
name(): LabeledInputPo {
|
||||
return LabeledInputPo.byLabel(this.self(), 'Name');
|
||||
}
|
||||
|
||||
ace(): ACE {
|
||||
return new ACE();
|
||||
}
|
||||
|
||||
resourceDetail() {
|
||||
return new ResourceDetailPo(this.self());
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.resourceDetail().createEditView().save();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,10 @@ export default class ChartRepositoriesListPo extends BaseResourceList {
|
|||
return this.resourceTable().sortableTable().rowActionMenuOpen(repoName);
|
||||
}
|
||||
|
||||
closeActionMenu() {
|
||||
cy.get('body').click(0, 0); // Click outside of the action menu
|
||||
}
|
||||
|
||||
openBulkActionDropdown() {
|
||||
return this.resourceTable().sortableTable().bulkActionDropDownOpen();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,8 @@ export default class FleetClusterList extends BaseResourceList {
|
|||
details(name: string, index: number) {
|
||||
return this.resourceTable().sortableTable().rowWithName(name).column(index);
|
||||
}
|
||||
|
||||
subRows() {
|
||||
return this.resourceTable().sortableTable().subRows();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||
import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po';
|
||||
import ResourceListMastheadPo from '@/cypress/e2e/po/components/ResourceList/resource-list-masthead.po';
|
||||
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
|
||||
import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po';
|
||||
import CruResourcePo from '@/cypress/e2e/po/components/cru-resource.po';
|
||||
|
||||
export class CisBenchmarkListPo extends PagePo {
|
||||
private static createPath(clusterId: string) {
|
||||
return `/c/${ clusterId }/cis/cis.cattle.io.clusterscan`;
|
||||
}
|
||||
|
||||
static goTo(clusterId: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
return super.goTo(CisBenchmarkListPo.createPath(clusterId));
|
||||
}
|
||||
|
||||
constructor(clusterId = 'local') {
|
||||
super(CisBenchmarkListPo.createPath(clusterId));
|
||||
}
|
||||
|
||||
masthead() {
|
||||
return new ResourceListMastheadPo(this.self());
|
||||
}
|
||||
|
||||
createScan() {
|
||||
return this.masthead().create();
|
||||
}
|
||||
|
||||
listElementWithName(name:string) {
|
||||
const baseResourceList = new BaseResourceList(this.self());
|
||||
|
||||
return baseResourceList.resourceTable().sortableTable().rowElementWithName(name);
|
||||
}
|
||||
|
||||
firstRow() {
|
||||
const baseResourceList = new BaseResourceList(this.self());
|
||||
|
||||
return baseResourceList.resourceTable().sortableTable().row(0);
|
||||
}
|
||||
}
|
||||
|
||||
export class CisBenchmarkPo extends PagePo {
|
||||
static url: string;
|
||||
|
||||
private static createPath( clusterId: string, name: string ) {
|
||||
const urlStr = `/c/${ clusterId }/cis/cis.cattle.io.clusterscan/${ name }`;
|
||||
|
||||
return urlStr;
|
||||
}
|
||||
|
||||
static goTo(): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
return super.goTo(this.url);
|
||||
}
|
||||
|
||||
constructor(clusterId = 'local', name = 'create') {
|
||||
super(CisBenchmarkPo.createPath(clusterId, name));
|
||||
|
||||
CisBenchmarkPo.url = CisBenchmarkPo.createPath(clusterId, name);
|
||||
}
|
||||
|
||||
cruResource() {
|
||||
return new CruResourcePo(this.self());
|
||||
}
|
||||
|
||||
nameNsDescription() {
|
||||
return new NameNsDescription(this.self());
|
||||
}
|
||||
|
||||
yamlEditor(): CodeMirrorPo {
|
||||
return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
|
|||
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
|
||||
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
|
||||
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
|
||||
import CreateEditNetworkPolicyPagePo from '@/cypress/e2e/po/edit/policy/network-policy.po';
|
||||
|
||||
export class NetworkPolicyPagePo extends PagePo {
|
||||
private static createPath(clusterId: string) {
|
||||
|
|
@ -21,7 +22,7 @@ export class NetworkPolicyPagePo extends PagePo {
|
|||
sideNav.navToSideMenuEntryByLabel('Network Policies');
|
||||
}
|
||||
|
||||
constructor(clusterId = 'local') {
|
||||
constructor(private clusterId = 'local') {
|
||||
super(NetworkPolicyPagePo.createPath(clusterId));
|
||||
}
|
||||
|
||||
|
|
@ -40,4 +41,8 @@ export class NetworkPolicyPagePo extends PagePo {
|
|||
searchForNetworkPolicy(name: string) {
|
||||
return this.list().resourceTable().sortableTable().filter(name);
|
||||
}
|
||||
|
||||
createEditNetworkPolicyForm(namespace?: string, id?: string): CreateEditNetworkPolicyPagePo {
|
||||
return new CreateEditNetworkPolicyPagePo(this.clusterId, namespace, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class ServicesPagePo extends PagePo {
|
|||
sideNav.navToSideMenuEntryByLabel('Service');
|
||||
}
|
||||
|
||||
constructor(clusterId = 'local') {
|
||||
constructor(private clusterId = 'local') {
|
||||
super(ServicesPagePo.createPath(clusterId));
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export class ServicesPagePo extends PagePo {
|
|||
return this.list().masthead().create();
|
||||
}
|
||||
|
||||
createServicesForm(id? : string): ServicesCreateEditPo {
|
||||
return new ServicesCreateEditPo(id);
|
||||
createServicesForm(namespace?: string, id?: string): ServicesCreateEditPo {
|
||||
return new ServicesCreateEditPo(this.clusterId, namespace, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default class ExtensionsPagePo extends PagePo {
|
|||
return this.title().should('contain', 'Extensions');
|
||||
}
|
||||
|
||||
loading(options: any) {
|
||||
loading() {
|
||||
return this.self().get('.data-loading');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export class HomeLinksPagePo extends RootClusterPage {
|
|||
}
|
||||
|
||||
addLinkButton() {
|
||||
return cy.getId('add_link_button');
|
||||
return cy.getId('add_row_item_button');
|
||||
}
|
||||
|
||||
removeLinkButton() {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ export class PerformancePagePo extends RootClusterPage {
|
|||
return CheckboxInputPo.byLabel(this.self(), 'Enable Garbage Collection');
|
||||
}
|
||||
|
||||
garbageCollectionResourceCount() {
|
||||
return LabeledInputPo.byLabel(this.self(), 'Resource Count');
|
||||
}
|
||||
|
||||
namespaceFilteringCheckbox(): CheckboxInputPo {
|
||||
return CheckboxInputPo.byLabel(this.self(), 'Enable Required Namespace / Project Filtering');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { ChartPage } from '@/cypress/e2e/po/pages/explorer/charts/chart.po';
|
||||
import HomePagePo from '@/cypress/e2e/po/pages/home.po';
|
||||
import { InstallChartPage } from '@/cypress/e2e/po/pages/explorer/charts/install-charts.po';
|
||||
import { MEDIUM_TIMEOUT_OPT, LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
import Kubectl from '@/cypress/e2e/po/components/kubectl.po';
|
||||
import { CisBenchmarkPo, CisBenchmarkListPo } from '@/cypress/e2e/po/other-products/cis-benchmark.po';
|
||||
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
|
||||
|
||||
describe('Charts', { testIsolation: 'off', tags: ['@charts', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
HomePagePo.goTo();
|
||||
});
|
||||
|
||||
describe('CIS Benchmark install', () => {
|
||||
const installChartPage = new InstallChartPage();
|
||||
const chartPage = new ChartPage();
|
||||
|
||||
describe('YAML view', () => {
|
||||
beforeEach(() => {
|
||||
ChartPage.navTo(null, 'CIS Benchmark');
|
||||
chartPage.waitForChartHeader('CIS Benchmark', MEDIUM_TIMEOUT_OPT);
|
||||
chartPage.goToInstall();
|
||||
installChartPage.nextPage().editYaml();
|
||||
});
|
||||
|
||||
describe('UI Elements', () => {
|
||||
it('Footer controls should sticky to bottom', () => {
|
||||
cy.get('#wizard-footer-controls').should('be.visible');
|
||||
|
||||
cy.get('#wizard-footer-controls').then(($el) => {
|
||||
const elementRect = $el[0].getBoundingClientRect();
|
||||
const viewportHeight = Cypress.config('viewportHeight');
|
||||
const pageHeight = Cypress.$(cy.state('window')).height();
|
||||
|
||||
expect(elementRect.bottom).to.eq(pageHeight);
|
||||
expect(elementRect.bottom).to.eq(viewportHeight);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CIS Chart setup', () => {
|
||||
it('Complete install and a Scan is created', () => {
|
||||
cy.updateNamespaceFilter('local', 'none', '{"local":[]}');
|
||||
const kubectl = new Kubectl();
|
||||
const cisBenchmark = new CisBenchmarkPo();
|
||||
const cisBenchmarkList = new CisBenchmarkListPo();
|
||||
const sideNav = new ProductNavPo();
|
||||
|
||||
ChartPage.navTo(null, 'CIS Benchmark');
|
||||
chartPage.waitForChartHeader('CIS Benchmark', MEDIUM_TIMEOUT_OPT);
|
||||
chartPage.goToInstall();
|
||||
|
||||
installChartPage.nextPage();
|
||||
|
||||
cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/rancher-charts?action=install').as('chartInstall');
|
||||
installChartPage.installChart();
|
||||
cy.wait('@chartInstall').its('response.statusCode').should('eq', 201);
|
||||
cy.contains('Disconnected');
|
||||
|
||||
kubectl.closeTerminal();
|
||||
|
||||
sideNav.navToSideMenuGroupByLabel('CIS Benchmark');
|
||||
cisBenchmarkList.waitForPage();
|
||||
cisBenchmarkList.createScan();
|
||||
cisBenchmark.waitForPage();
|
||||
cisBenchmark.cruResource().saveAndWaitForRequests('POST', 'v1/cis.cattle.io.clusterscans')
|
||||
.then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(201);
|
||||
expect(response?.body).to.have.property('type', 'cis.cattle.io.clusterscan');
|
||||
expect(response?.body.metadata).to.have.property('name');
|
||||
expect(response?.body.metadata).to.have.property('generateName', 'scan-');
|
||||
});
|
||||
cisBenchmarkList.waitForPage();
|
||||
cisBenchmarkList.checkVisible();
|
||||
const column = cisBenchmarkList.firstRow().column(1);
|
||||
|
||||
column.get('.bg-success', LONG_TIMEOUT_OPT).should('exist');
|
||||
});
|
||||
|
||||
after('clean up', () => {
|
||||
const chartNamespace = 'cis-operator-system';
|
||||
const chartApp = 'rancher-cis-benchmark';
|
||||
const chartCrd = 'rancher-cis-benchmark-crd';
|
||||
|
||||
cy.createRancherResource('v1', `catalog.cattle.io.apps/${ chartNamespace }/${ chartApp }?action=uninstall`, '{}');
|
||||
cy.createRancherResource('v1', `catalog.cattle.io.apps/${ chartNamespace }/${ chartCrd }?action=uninstall`, '{}');
|
||||
cy.updateNamespaceFilter('local', 'none', '{"local":["all://user"]}');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -142,4 +142,29 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
|
|||
|
||||
cy.get('@fetchChartDataAfterBack.all').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('A disabled repo should NOT be listed on the repos dropdown', () => {
|
||||
const disabledRepoId = 'disabled-repo';
|
||||
|
||||
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos?exclude=metadata.managedFields', (req) => {
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
data: [
|
||||
{ id: disabledRepoId, spec: { enabled: false } }, // disabled
|
||||
{ id: 'enabled-repo-1', spec: { enabled: true } }, // enabled
|
||||
{ id: 'enabled-repo-2', spec: {} } // enabled
|
||||
]
|
||||
}
|
||||
});
|
||||
}).as('getRepos');
|
||||
|
||||
cy.wait('@getRepos');
|
||||
|
||||
chartsPage.chartsFilterReposSelect().toggle();
|
||||
chartsPage.chartsFilterReposSelect().isOpened();
|
||||
chartsPage.chartsFilterReposSelect().getOptions().should('have.length', 3); // should include three options: All, enabled-repo-1 and enabled-repo-2
|
||||
chartsPage.chartsFilterReposSelect().getOptions().contains(disabledRepoId)
|
||||
.should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,80 +1,126 @@
|
|||
import { NetworkPolicyPagePo } from '@/cypress/e2e/po/pages/explorer/network-policy.po';
|
||||
import NetworkPolicyPo from '@/cypress/e2e/po/components/policy/network-policy.po';
|
||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||
|
||||
const networkPolicyPage = new NetworkPolicyPagePo('local');
|
||||
const networkPolicyName = 'custom-network-policy';
|
||||
const customNetworkPolicyName = 'custom-network-policy';
|
||||
const networkPolicyDescription = 'Custom Network Policy Description';
|
||||
const namespace = 'default';
|
||||
let networkPolicyName = '';
|
||||
let removeNetworkPolicy = false;
|
||||
const networkPolicyToDelete = [];
|
||||
|
||||
describe('NetworkPolicies', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.createE2EResourceName('networkpolicy').then((name) => {
|
||||
networkPolicyName = name;
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a network policy and displays it in the list', () => {
|
||||
// Visit the main menu and select the 'local' cluster
|
||||
// Navigate to Policy => Network Policies
|
||||
NetworkPolicyPagePo.navTo();
|
||||
networkPolicyPage.waitForPage();
|
||||
// Go to Create Page
|
||||
networkPolicyPage.clickCreate();
|
||||
const networkPolicyPo = new NetworkPolicyPo();
|
||||
|
||||
// Enter name & description
|
||||
networkPolicyPo.nameInput().set(networkPolicyName);
|
||||
networkPolicyPo.descriptionInput().set(networkPolicyDescription);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().nameInput().set(customNetworkPolicyName);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().descriptionInput().set(networkPolicyDescription);
|
||||
// Enable ingress checkbox
|
||||
networkPolicyPo.enableIngressCheckbox().set();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().enableIngressCheckbox().set();
|
||||
// Add a new rule without a key to match all the namespaces
|
||||
networkPolicyPo.newNetworkPolicyRuleAddBtn().click();
|
||||
networkPolicyPo.addAllowedTrafficSourceButton().click();
|
||||
networkPolicyPo.policyRuleTargetSelect(0).toggle();
|
||||
networkPolicyPo.policyRuleTargetSelect(0).clickOptionWithLabel('Namespace Selector');
|
||||
networkPolicyPage.createEditNetworkPolicyForm().newNetworkPolicyRuleAddBtn().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().addAllowedTrafficSourceButton().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(0).toggle();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(0).clickOptionWithLabel('Namespace Selector');
|
||||
|
||||
cy.getRancherResource('v1', 'namespaces').then((resp: Cypress.Response<any>) => {
|
||||
cy.wrap(resp.body.count).as('namespaceCount');
|
||||
});
|
||||
|
||||
cy.get('@namespaceCount').then((count) => {
|
||||
networkPolicyPo.matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
||||
// Add a second rule a key to match none of the namespaces
|
||||
networkPolicyPo.addAllowedTrafficSourceButton().click();
|
||||
networkPolicyPo.policyRuleTargetSelect(1).toggle();
|
||||
networkPolicyPo.policyRuleTargetSelect(1).clickOptionWithLabel('Namespace Selector');
|
||||
networkPolicyPo.policyRuleKeyInput(1).focus().type('something-with-no-matching-namespaces');
|
||||
networkPolicyPo.matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().addAllowedTrafficSourceButton().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(1).toggle();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(1).clickOptionWithLabel('Namespace Selector');
|
||||
networkPolicyPage.createEditNetworkPolicyForm().policyRuleKeyInput(1).focus().type('something-with-no-matching-namespaces');
|
||||
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||
// Click on Create
|
||||
networkPolicyPo.saveCreateForm().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().saveCreateForm().click();
|
||||
// Check if the NetworkPolicy is created successfully
|
||||
networkPolicyPage.waitForPage();
|
||||
networkPolicyPage.searchForNetworkPolicy(networkPolicyName);
|
||||
networkPolicyPage.waitForPage(`q=${ networkPolicyName }`);
|
||||
networkPolicyPage.listElementWithName(networkPolicyName).should('exist').and('be.visible');
|
||||
networkPolicyPage.searchForNetworkPolicy(customNetworkPolicyName);
|
||||
networkPolicyPage.waitForPage(`q=${ customNetworkPolicyName }`);
|
||||
networkPolicyPage.listElementWithName(customNetworkPolicyName).should('exist').and('be.visible');
|
||||
// Navigate back to the edit page and check if the matching message is still correct
|
||||
networkPolicyPage.list().actionMenu(networkPolicyName).getMenuItem('Edit Config').click();
|
||||
networkPolicyPo.matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
||||
networkPolicyPo.matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||
networkPolicyPage.list().actionMenu(customNetworkPolicyName).getMenuItem('Edit Config').click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||
});
|
||||
});
|
||||
|
||||
it('can open "Edit as YAML"', () => {
|
||||
NetworkPolicyPagePo.navTo();
|
||||
networkPolicyPage.waitForPage();
|
||||
networkPolicyPage.clickCreate();
|
||||
const networkPolicyPo = new NetworkPolicyPo();
|
||||
|
||||
networkPolicyPo.editAsYaml().click();
|
||||
networkPolicyPo.yamlEditor().checkExists();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().editAsYaml().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().yamlEditor().checkExists();
|
||||
});
|
||||
|
||||
// testing https://github.com/rancher/dashboard/issues/11856
|
||||
it('port value is sent correctly in request payload', () => {
|
||||
cy.intercept('POST', 'v1/networking.k8s.io.networkpolicies').as('createNetworkPolicy');
|
||||
|
||||
NetworkPolicyPagePo.navTo();
|
||||
networkPolicyPage.waitForPage();
|
||||
networkPolicyPage.clickCreate();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().waitForPage(null, 'ingress');
|
||||
|
||||
networkPolicyPage.createEditNetworkPolicyForm().nameInput().set(networkPolicyName);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().descriptionInput().set(networkPolicyDescription);
|
||||
networkPolicyPage.createEditNetworkPolicyForm().enableIngressCheckbox().set();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().newNetworkPolicyRuleAddBtn().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().addAllowedPortButton().click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm().ingressRuleItem(0).find('.col:nth-of-type(1) input').type('8080');
|
||||
networkPolicyPage.createEditNetworkPolicyForm().saveCreateForm().click();
|
||||
|
||||
// check request payload
|
||||
cy.wait('@createNetworkPolicy').then(({ request, response }) => {
|
||||
expect(response?.statusCode).to.eq(201);
|
||||
removeNetworkPolicy = true;
|
||||
networkPolicyToDelete.push(`${ namespace }/${ networkPolicyName }`);
|
||||
expect(request?.body.spec.ingress).to.deep.include({ ports: [{ port: 8080 }] });
|
||||
});
|
||||
networkPolicyPage.waitForPage();
|
||||
networkPolicyPage.searchForNetworkPolicy(networkPolicyName);
|
||||
networkPolicyPage.waitForPage(`q=${ networkPolicyName }`);
|
||||
networkPolicyPage.list().actionMenu(networkPolicyName).getMenuItem('Edit Config').click();
|
||||
networkPolicyPage.createEditNetworkPolicyForm(namespace, networkPolicyName).waitForPage(`mode=edit#rule-ingress0`);
|
||||
// check elements value property
|
||||
networkPolicyPage.createEditNetworkPolicyForm().ingressRuleItem(0).find('.col:nth-of-type(1) input').should('have.value', '8080');
|
||||
});
|
||||
|
||||
it('can delete a network policy', () => {
|
||||
NetworkPolicyPagePo.navTo();
|
||||
networkPolicyPage.waitForPage();
|
||||
networkPolicyPage.list().actionMenu(networkPolicyName).getMenuItem('Delete').click();
|
||||
networkPolicyPage.list().actionMenu(customNetworkPolicyName).getMenuItem('Delete').click();
|
||||
const promptRemove = new PromptRemove();
|
||||
|
||||
cy.intercept('DELETE', 'v1/networking.k8s.io.networkpolicies/**').as('deleteNetworkPolicy');
|
||||
promptRemove.remove();
|
||||
cy.wait('@deleteNetworkPolicy');
|
||||
networkPolicyPage.waitForPage();
|
||||
cy.contains(networkPolicyName).should('not.exist');
|
||||
cy.contains(customNetworkPolicyName).should('not.exist');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (removeNetworkPolicy) {
|
||||
// delete gitrepo
|
||||
networkPolicyToDelete.forEach((r) => cy.deleteRancherResource('v1', 'networking.k8s.io.networkpolicies', r, false));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,147 @@
|
|||
import { ServicesPagePo } from '@/cypress/e2e/po/pages/explorer/services.po';
|
||||
import { generateServicesDataSmall, servicesNoData } from '@/cypress/e2e/blueprints/explorer/workloads/service-discovery/services-get';
|
||||
import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po';
|
||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||
import { GrowlManagerPo } from '@/cypress/e2e/po/components/growl-manager.po';
|
||||
|
||||
const cluster = 'local';
|
||||
const servicesPagePo = new ServicesPagePo();
|
||||
const growlPo = new GrowlManagerPo();
|
||||
const cluster = 'local';
|
||||
let serviceExternalName = '';
|
||||
const namespace = 'default';
|
||||
let removeServices = false;
|
||||
const servicesToDelete = [];
|
||||
|
||||
describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.createE2EResourceName('serviceexternalname').then((name) => {
|
||||
serviceExternalName = name;
|
||||
});
|
||||
});
|
||||
|
||||
describe('CRUD', () => {
|
||||
it('can create an ExternalName Service', () => {
|
||||
cy.intercept('POST', '/v1/services').as('createService');
|
||||
ServicesPagePo.navTo();
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.clickCreate();
|
||||
servicesPagePo.createServicesForm().waitForPage();
|
||||
|
||||
servicesPagePo.createServicesForm().selectServiceOption(1);
|
||||
servicesPagePo.createServicesForm().waitForPage(null, 'define-external-name');
|
||||
servicesPagePo.createServicesForm().resourceDetail().title().should('contain', 'Create ExternalName');
|
||||
servicesPagePo.createServicesForm().nameNsDescription().name().set(serviceExternalName);
|
||||
servicesPagePo.createServicesForm().nameNsDescription().description().set(`${ serviceExternalName }-desc`);
|
||||
servicesPagePo.createServicesForm().selectNamespace(namespace);
|
||||
servicesPagePo.createServicesForm().tabs().allTabs().should('have.length', 3);
|
||||
|
||||
const tabs = ['External Name', 'IP Addresses', 'Labels & Annotations'];
|
||||
|
||||
servicesPagePo.createServicesForm().tabs().tabNames().each((el, i) => {
|
||||
expect(el).to.eq(tabs[i]);
|
||||
});
|
||||
|
||||
servicesPagePo.createServicesForm().tabs().assertTabIsActive('[data-testid="define-external-name"]');
|
||||
servicesPagePo.createServicesForm().externalNameInput().set('my.database.example.com');
|
||||
servicesPagePo.createServicesForm().ipAddressesTab();
|
||||
servicesPagePo.createServicesForm().waitForPage(null, 'ips');
|
||||
servicesPagePo.createServicesForm().ipAddressList().setValueAtIndex('1.1.1.1', 0);
|
||||
servicesPagePo.createServicesForm().ipAddressList().setValueAtIndex('2.2.2.2', 1);
|
||||
servicesPagePo.createServicesForm().lablesAnnotationsTab();
|
||||
servicesPagePo.createServicesForm().waitForPage(null, 'labels-and-annotations');
|
||||
servicesPagePo.createServicesForm().lablesAnnotationsKeyValue().setKeyValueAtIndex('Add Label', 'label-key1', 'label-value1', 0, '.labels-and-annotations-container div.row:nth-of-type(2)');
|
||||
|
||||
// Adding Annotations doesn't work via test automation
|
||||
// See https://github.com/rancher/dashboard/issues/13191
|
||||
// servicesPagePo.createServicesForm().lablesAnnotationsKeyValue().setKeyValueAtIndex('Add Annotation', 'ann-key1', 'ann-value1', 0, '.labels-and-annotations-container div.row:nth-of-type(3)');
|
||||
servicesPagePo.createServicesForm().resourceDetail().createEditView().create();
|
||||
cy.wait('@createService').then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(201);
|
||||
removeServices = true;
|
||||
servicesToDelete.push(`${ namespace }/${ serviceExternalName }`);
|
||||
});
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().resourceTable().sortableTable().rowWithName(serviceExternalName)
|
||||
.checkVisible();
|
||||
growlPo.dismissWarning();
|
||||
});
|
||||
|
||||
it('can edit an ExternalName Service', () => {
|
||||
ServicesPagePo.navTo();
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().actionMenu(serviceExternalName).getMenuItem('Edit Config').click();
|
||||
servicesPagePo.createServicesForm(namespace, serviceExternalName).waitForPage('mode=edit', 'define-external-name');
|
||||
servicesPagePo.createServicesForm().nameNsDescription().description().set(`${ serviceExternalName }-desc`);
|
||||
servicesPagePo.createServicesForm().resourceDetail().cruResource().saveAndWaitForRequests('PUT', `/v1/services/${ namespace }/${ serviceExternalName }`)
|
||||
.then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(200);
|
||||
expect(response?.body.metadata).to.have.property('name', serviceExternalName);
|
||||
expect(response?.body.metadata.annotations).to.have.property('field.cattle.io/description', `${ serviceExternalName }-desc`);
|
||||
});
|
||||
servicesPagePo.waitForPage();
|
||||
growlPo.dismissWarning();
|
||||
});
|
||||
|
||||
it('can clone an ExternalName Service', () => {
|
||||
ServicesPagePo.navTo();
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().actionMenu(serviceExternalName).getMenuItem('Clone').click();
|
||||
servicesPagePo.createServicesForm(namespace, serviceExternalName).waitForPage('mode=clone', 'define-external-name');
|
||||
servicesPagePo.createServicesForm().nameNsDescription().name().set(`clone-${ serviceExternalName }`);
|
||||
servicesPagePo.createServicesForm().resourceDetail().cruResource().saveAndWaitForRequests('POST', '/v1/services')
|
||||
.then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(201);
|
||||
expect(response?.body.metadata).to.have.property('name', `clone-${ serviceExternalName }`);
|
||||
removeServices = true;
|
||||
servicesToDelete.push(`${ namespace }/clone-${ serviceExternalName }`);
|
||||
});
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().resourceTable().sortableTable().rowWithName(`clone-${ serviceExternalName }`)
|
||||
.checkVisible();
|
||||
growlPo.dismissWarning();
|
||||
});
|
||||
|
||||
it('can Edit Yaml', () => {
|
||||
ServicesPagePo.navTo();
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().actionMenu(`clone-${ serviceExternalName }`).getMenuItem('Edit YAML').click();
|
||||
servicesPagePo.createServicesForm(namespace, `clone-${ serviceExternalName }`).waitForPage('mode=edit&as=yaml');
|
||||
servicesPagePo.createServicesForm().title().contains(`Service: clone-${ serviceExternalName }`).should('be.visible');
|
||||
});
|
||||
|
||||
it('can delete an ExternalName Service', () => {
|
||||
ServicesPagePo.navTo();
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().actionMenu(`clone-${ serviceExternalName }`).getMenuItem('Delete').click();
|
||||
servicesPagePo.list().resourceTable().sortableTable().rowNames('.col-link-detail')
|
||||
.then((rows: any) => {
|
||||
const promptRemove = new PromptRemove();
|
||||
|
||||
cy.intercept('DELETE', `/v1/services/${ namespace }/clone-${ serviceExternalName }`).as('deleteService');
|
||||
|
||||
promptRemove.remove();
|
||||
cy.wait('@deleteService');
|
||||
servicesPagePo.waitForPage();
|
||||
servicesPagePo.list().resourceTable().sortableTable().checkRowCount(false, rows.length - 1);
|
||||
servicesPagePo.list().resourceTable().sortableTable().rowNames('.col-link-detail')
|
||||
.should('not.contain', `clone-${ serviceExternalName }`);
|
||||
});
|
||||
});
|
||||
|
||||
// testing https://github.com/rancher/dashboard/issues/11889
|
||||
it('validation errors should not be shown when form is just opened', () => {
|
||||
servicesPagePo.goTo();
|
||||
servicesPagePo.clickCreate();
|
||||
servicesPagePo.createServicesForm().errorBanner().should('not.exist');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (removeServices) {
|
||||
// delete gitrepo
|
||||
servicesToDelete.forEach((r) => cy.deleteRancherResource('v1', 'services', r, false));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('List', { tags: ['@vai', '@adminUser'] }, () => {
|
||||
|
|
@ -84,12 +218,6 @@ describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }
|
|||
servicesPagePo.list().resourceTable().sortableTable().checkRowCount(false, 3);
|
||||
});
|
||||
|
||||
it('validation errors should not be shown when form is just opened', () => {
|
||||
servicesPagePo.goTo();
|
||||
servicesPagePo.clickCreate();
|
||||
servicesPagePo.createServicesForm().errorBanner().should('not.exist');
|
||||
});
|
||||
|
||||
after('clean up', () => {
|
||||
cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
// Ensure that the banner should be shown (by confirming that a required repo isn't there)
|
||||
appRepoList.goTo();
|
||||
appRepoList.waitForPage();
|
||||
appRepoList.sortableTable().checkLoadingIndicatorNotVisible();
|
||||
appRepoList.sortableTable().noRowsShouldNotExist();
|
||||
appRepoList.sortableTable().rowNames().then((names: any) => {
|
||||
if (names.includes(UI_PLUGINS_PARTNERS_REPO_NAME)) {
|
||||
|
|
@ -409,6 +410,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
|
||||
extensionsPo.extensionTabAvailableClick();
|
||||
extensionsPo.waitForPage(null, 'available');
|
||||
extensionsPo.loading().should('not.exist');
|
||||
|
||||
// Install unauthenticated extension
|
||||
extensionsPo.extensionCardInstallClick(UNAUTHENTICATED_EXTENSION_NAME);
|
||||
|
|
@ -418,6 +420,8 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
// let's check the extension reload banner and reload the page
|
||||
extensionsPo.extensionReloadBanner().should('be.visible');
|
||||
extensionsPo.extensionReloadClick();
|
||||
extensionsPo.waitForPage(null, 'installed');
|
||||
extensionsPo.loading().should('not.exist');
|
||||
|
||||
// make sure both extensions have been imported
|
||||
extensionsPo.extensionScriptImport(UNAUTHENTICATED_EXTENSION_NAME).should('exist');
|
||||
|
|
@ -436,7 +440,8 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
// make sure both extensions have been imported after logging in again
|
||||
cy.login(undefined, undefined, false);
|
||||
extensionsPo.goTo();
|
||||
extensionsPo.waitForPage();
|
||||
extensionsPo.waitForPage(null, 'installed');
|
||||
extensionsPo.loading().should('not.exist');
|
||||
extensionsPo.waitForTitle();
|
||||
extensionsPo.extensionScriptImport(UNAUTHENTICATED_EXTENSION_NAME).should('exist');
|
||||
extensionsPo.extensionScriptImport(EXTENSION_NAME).should('exist');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ describe('Workspaces', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] },
|
|||
let initialCount: number;
|
||||
|
||||
it('check table headers are available in list and details view', () => {
|
||||
FleetWorkspaceListPagePo.navTo();
|
||||
fleetWorkspacesPage.goTo();
|
||||
fleetWorkspacesPage.waitForPage();
|
||||
fleetWorkspacesPage.sortableTable().noRowsShouldNotExist();
|
||||
fleetWorkspacesPage.sortableTable().filter(defaultWorkspace);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
|
|||
rke2ClusterAmazon: {
|
||||
clusterName: name,
|
||||
namespace,
|
||||
}
|
||||
},
|
||||
metadata: { labels: { foo: 'bar' } }
|
||||
}).then(() => {
|
||||
removeCluster = true;
|
||||
});
|
||||
|
|
@ -123,6 +124,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
|
|||
fleetClusterListPage.clusterList().details(clusterName, 4).should('have.text', '1');
|
||||
// check resources: testing https://github.com/rancher/dashboard/issues/11154
|
||||
fleetClusterListPage.clusterList().details(clusterName, 5).contains( ' 1 ', MEDIUM_TIMEOUT_OPT);
|
||||
// check cluster labels
|
||||
fleetClusterListPage.clusterList().subRows().should('contain.text', 'foo=bar');
|
||||
|
||||
const fleetClusterDetailsPage = new FleetClusterDetailsPo(namespace, clusterName);
|
||||
|
||||
|
|
|
|||
|
|
@ -144,12 +144,18 @@ describe('Performance', { testIsolation: 'off', tags: ['@globalSettings', '@admi
|
|||
performancePage.garbageCollectionCheckbox().isUnchecked();
|
||||
performancePage.garbageCollectionCheckbox().set();
|
||||
performancePage.garbageCollectionCheckbox().isChecked();
|
||||
// testing https://github.com/rancher/dashboard/issues/11856
|
||||
performancePage.garbageCollectionResourceCount().set('600');
|
||||
performancePage.applyAndWait('garbageCollection-true').then(({ request, response }) => {
|
||||
expect(response?.statusCode).to.eq(200);
|
||||
expect(request.body).to.have.property('value').contains('\"garbageCollection\":{\"enabled\":true');
|
||||
expect(response?.body).to.have.property('value').contains('\"garbageCollection\":{\"enabled\":true');
|
||||
expect(response?.body).to.have.property('value').contains('\"countThreshold\":600');
|
||||
});
|
||||
|
||||
// check elements value property
|
||||
performancePage.garbageCollectionResourceCount().shouldHaveValue('600');
|
||||
|
||||
// Disable garbage collection
|
||||
performancePage.garbageCollectionCheckbox().isChecked();
|
||||
performancePage.garbageCollectionCheckbox().set();
|
||||
|
|
@ -10,7 +10,7 @@ import ClusterManagerDetailImportedGenericPagePo from '@/cypress/e2e/po/detail/p
|
|||
import ClusterManagerCreateRke2CustomPagePo from '@/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create-rke2-custom.po';
|
||||
import ClusterManagerEditRke2CustomPagePo from '@/cypress/e2e/po/edit/provisioning.cattle.io.cluster/edit/cluster-edit-rke2-custom.po';
|
||||
import ClusterManagerImportGenericPagePo from '@/cypress/e2e/po/edit/provisioning.cattle.io.cluster/import/cluster-import.generic.po';
|
||||
import ClusterManagerEditGenericPagePo from '@/cypress/e2e/po/edit/provisioning.cattle.io.cluster/edit/cluster-edit-generic.po';
|
||||
import ClusterManagerEditImportedPagePo from '@/cypress/e2e/po/extensions/imported/cluster-edit.po';
|
||||
import ClusterManagerNamespacePagePo from '@/cypress/e2e/po/pages/cluster-manager/namespace.po';
|
||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||
import * as path from 'path';
|
||||
|
|
@ -563,9 +563,11 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
|
||||
describe('Imported', { tags: ['@jenkins', '@importedCluster'] }, () => {
|
||||
const importClusterPage = new ClusterManagerImportGenericPagePo();
|
||||
const fqdn = 'fqdn';
|
||||
const cacert = 'cacert';
|
||||
|
||||
describe('Generic', () => {
|
||||
const editImportedClusterPage = new ClusterManagerEditGenericPagePo(undefined, importGenericName);
|
||||
const editImportedClusterPage = new ClusterManagerEditImportedPagePo(undefined, importGenericName);
|
||||
|
||||
it('can create new cluster', () => {
|
||||
const detailClusterPage = new ClusterManagerDetailImportedGenericPagePo(undefined, importGenericName);
|
||||
|
|
@ -614,11 +616,30 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
clusterList.list().providerSubType(importGenericName).should('contain.text', 'K3s');
|
||||
});
|
||||
|
||||
it('can navigate to cluster edit page', () => {
|
||||
it('can edit imported cluster and see changes afterwards', () => {
|
||||
cy.intercept('GET', '/v1-rke2-release/releases').as('getRke2Releases');
|
||||
clusterList.goTo();
|
||||
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click();
|
||||
editImportedClusterPage.waitForPage('mode=edit');
|
||||
|
||||
editImportedClusterPage.name().value().should('eq', importGenericName );
|
||||
// Issue #10432: Edit Cluster screen falsely gives impression imported cluster's name and description can be edited
|
||||
editImportedClusterPage.name().expectToBeDisabled();
|
||||
|
||||
editImportedClusterPage.ace().enable();
|
||||
editImportedClusterPage.ace().enterFdqn(fqdn);
|
||||
editImportedClusterPage.ace().enterCaCerts(cacert);
|
||||
|
||||
editImportedClusterPage.save();
|
||||
|
||||
// We should be taken back to the list page if the save was successful
|
||||
clusterList.waitForPage();
|
||||
|
||||
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click();
|
||||
|
||||
editImportedClusterPage.waitForPage('mode=edit');
|
||||
editImportedClusterPage.ace().fqdn().value().should('eq', fqdn );
|
||||
editImportedClusterPage.ace().caCerts().value().should('eq', cacert );
|
||||
});
|
||||
|
||||
it('can delete cluster by bulk actions', () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import * as jsyaml from 'js-yaml';
|
|||
import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
|
||||
const chartBranch = `release-v${ CURRENT_RANCHER_VERSION }`;
|
||||
const gitRepoUrl = 'https://github.com/rancher/charts';
|
||||
|
||||
describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: ['@manager', '@adminUser'] }, () => {
|
||||
const repositoriesPage = new ChartRepositoriesPagePo(undefined, 'manager');
|
||||
|
|
@ -27,7 +28,7 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
|||
repositoriesPage.createEditRepositories().nameNsDescription().name().set(this.repoName);
|
||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set('https://github.com/rancher/charts');
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set(gitRepoUrl);
|
||||
repositoriesPage.createEditRepositories().gitBranch().set(chartBranch);
|
||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos').its('response.statusCode').should('eq', 201);
|
||||
repositoriesPage.waitForPage();
|
||||
|
|
@ -129,7 +130,7 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
|||
repositoriesPage.createEditRepositories().nameNsDescription().name().set(`${ this.repoName }basic`);
|
||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set('https://github.com/rancher/charts');
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set(gitRepoUrl);
|
||||
repositoriesPage.createEditRepositories().gitBranch().set(chartBranch);
|
||||
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createBasicAuth('test', 'test');
|
||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos');
|
||||
|
|
@ -148,7 +149,7 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
|||
repositoriesPage.createEditRepositories().nameNsDescription().name().set(`${ this.repoName }ssh`);
|
||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set('https://github.com/rancher/charts');
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set(gitRepoUrl);
|
||||
repositoriesPage.createEditRepositories().gitBranch().set(chartBranch);
|
||||
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createSSHAuth('privateKey', 'publicKey');
|
||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos');
|
||||
|
|
@ -254,4 +255,55 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
|||
// check list details
|
||||
cy.contains(this.repoName).should('not.exist');
|
||||
});
|
||||
|
||||
it('can disable/enable a repository', function() {
|
||||
// create repo
|
||||
ChartRepositoriesPagePo.navTo();
|
||||
repositoriesPage.waitForPage();
|
||||
repositoriesPage.create();
|
||||
repositoriesPage.createEditRepositories().waitForPage();
|
||||
repositoriesPage.createEditRepositories().nameNsDescription().name().set(this.repoName);
|
||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
||||
repositoriesPage.createEditRepositories().gitRepoUrl().set(gitRepoUrl);
|
||||
repositoriesPage.createEditRepositories().gitBranch().set(chartBranch);
|
||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos').its('response.statusCode').should('eq', 201);
|
||||
repositoriesPage.waitForPage();
|
||||
|
||||
// check list details
|
||||
repositoriesPage.list().details(this.repoName, 2).should('be.visible');
|
||||
repositoriesPage.list().details(this.repoName, 1).contains('In Progress').should('be.visible');
|
||||
|
||||
// refresh should be displayed for an enabled repo
|
||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('be.visible');
|
||||
// close action menu
|
||||
repositoriesPage.list().closeActionMenu();
|
||||
// disable repo
|
||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Disable').click();
|
||||
repositoriesPage.list().details(this.repoName, 1).contains('Disabled', { timeout: 10000 }).scrollIntoView()
|
||||
.should('be.visible');
|
||||
|
||||
// refresh should NOT be displayed for a disabled repo
|
||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Refresh').should('not.exist');
|
||||
// close action menu
|
||||
repositoriesPage.list().closeActionMenu();
|
||||
// enable repo
|
||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Enable').click();
|
||||
repositoriesPage.list().details(this.repoName, 1).contains('Active', LONG_TIMEOUT_OPT).scrollIntoView()
|
||||
.should('be.visible');
|
||||
|
||||
// delete repo
|
||||
repositoriesPage.list().actionMenu(this.repoName).getMenuItem('Delete').click();
|
||||
|
||||
const promptRemove = new PromptRemove();
|
||||
|
||||
cy.intercept('DELETE', `v1/catalog.cattle.io.clusterrepos/${ this.repoName }`).as('deleteRepository');
|
||||
|
||||
promptRemove.remove();
|
||||
cy.wait('@deleteRepository');
|
||||
repositoriesPage.waitForPage();
|
||||
|
||||
// check list details
|
||||
cy.contains(this.repoName).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export type CreateAmazonRke2ClusterParams = {
|
|||
type: string,
|
||||
clusterName: string,
|
||||
namespace: string
|
||||
},
|
||||
},
|
||||
cloudCredentialsAmazon: {
|
||||
workspace: string,
|
||||
name: string,
|
||||
|
|
@ -40,7 +40,11 @@ export type CreateAmazonRke2ClusterParams = {
|
|||
rke2ClusterAmazon: {
|
||||
clusterName: string,
|
||||
namespace: string,
|
||||
}
|
||||
},
|
||||
metadata?: {
|
||||
labels?: { [key: string]: string },
|
||||
annotations?: { [key: string]: string },
|
||||
},
|
||||
}
|
||||
export type CreateAmazonRke2ClusterWithoutMachineConfigParams = {
|
||||
cloudCredentialsAmazon: {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ node {
|
|||
string(credentialsId: 'AWS_SECRET_ACCESS_KEY', variable: 'AWS_SECRET_ACCESS_KEY'),
|
||||
string(credentialsId: 'AZURE_AKS_SUBSCRIPTION_ID', variable: 'AZURE_AKS_SUBSCRIPTION_ID'),
|
||||
string(credentialsId: 'AZURE_CLIENT_ID', variable: 'AZURE_CLIENT_ID'),
|
||||
string(credentialsId: 'AZURE_CLIENT_SECRET', variable: 'AZURE_CLIENT_SECRET')]) {
|
||||
string(credentialsId: 'AZURE_CLIENT_SECRET', variable: 'AZURE_CLIENT_SECRET'),
|
||||
string(credentialsId: 'GKE_SERVICE_ACCOUNT', variable: 'GKE_SERVICE_ACCOUNT')
|
||||
]) {
|
||||
withEnv(paramsMap) {
|
||||
stage('Checkout') {
|
||||
deleteDir()
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ export default defineConfig({
|
|||
azureClientId: process.env.AZURE_CLIENT_ID,
|
||||
azureClientSecret: process.env.AZURE_CLIENT_SECRET,
|
||||
customNodeIp: process.env.CUSTOM_NODE_IP,
|
||||
customNodeKey: process.env.CUSTOM_NODE_KEY
|
||||
customNodeKey: process.env.CUSTOM_NODE_KEY,
|
||||
gkeServiceAccount: process.env.GKE_SERVICE_ACCOUNT
|
||||
},
|
||||
// Jenkins reporters configuration jUnit and HTML
|
||||
reporter: 'cypress-multi-reporters',
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ corral config vars set azure_subscription_id "${AZURE_AKS_SUBSCRIPTION_ID}"
|
|||
corral config vars set azure_client_id "${AZURE_CLIENT_ID}"
|
||||
corral config vars set azure_client_secret "${AZURE_CLIENT_SECRET}"
|
||||
corral config vars set create_initial_clusters "${CREATE_INITIAL_CLUSTERS}"
|
||||
corral config vars set gke_service_account "${GKE_SERVICE_ACCOUNT}"
|
||||
|
||||
create_initial_clusters() {
|
||||
shopt -u nocasematch
|
||||
|
|
|
|||
|
|
@ -602,7 +602,9 @@ Cypress.Commands.add('deleteNodeTemplate', (nodeTemplateId, timeout = 30000, fai
|
|||
* Create RKE2 cluster with Amazon EC2 cloud provider
|
||||
*/
|
||||
Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2ClusterParams) => {
|
||||
const { machineConfig, rke2ClusterAmazon, cloudCredentialsAmazon } = params;
|
||||
const {
|
||||
machineConfig, rke2ClusterAmazon, cloudCredentialsAmazon, metadata
|
||||
} = params;
|
||||
|
||||
return cy.createAwsCloudCredentials(cloudCredentialsAmazon.workspace, cloudCredentialsAmazon.name, cloudCredentialsAmazon.region, cloudCredentialsAmazon.accessKey, cloudCredentialsAmazon.secretKey)
|
||||
.then((resp: Cypress.Response<any>) => {
|
||||
|
|
@ -625,7 +627,11 @@ Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2Cluster
|
|||
type: 'provisioning.cattle.io.cluster',
|
||||
metadata: {
|
||||
namespace: rke2ClusterAmazon.namespace,
|
||||
annotations: { 'field.cattle.io/description': `${ rke2ClusterAmazon.clusterName }-description` },
|
||||
annotations: {
|
||||
'field.cattle.io/description': `${ rke2ClusterAmazon.clusterName }-description`,
|
||||
...(metadata?.annotations || {}),
|
||||
},
|
||||
labels: metadata?.labels || {},
|
||||
name: rke2ClusterAmazon.clusterName
|
||||
},
|
||||
spec: {
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -76,7 +76,7 @@
|
|||
"cookie": "0.7.0",
|
||||
"cookie-universal": "2.2.2",
|
||||
"cron-validator": "1.2.0",
|
||||
"cronstrue": "1.95.0",
|
||||
"cronstrue": "2.53.0",
|
||||
"cross-env": "7.0.3",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"d3": "7.3.0",
|
||||
|
|
@ -90,6 +90,7 @@
|
|||
"express": "4.17.1",
|
||||
"file-saver": "2.0.2",
|
||||
"floating-vue": "5.2.2",
|
||||
"focus-trap": "7.6.2",
|
||||
"https": "1.0.0",
|
||||
"identicon.js": "2.3.3",
|
||||
"intl-messageformat": "7.8.4",
|
||||
|
|
@ -135,7 +136,7 @@
|
|||
"yaml": "2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.14.5",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "7.23.4",
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "24.4.0",
|
||||
"eslint-plugin-local-rules": "link:./eslint-plugin-local-rules",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-vue": "9.10.0",
|
||||
"eslint": "7.32.0",
|
||||
|
|
@ -188,7 +190,7 @@
|
|||
"lodash.debounce": "4.0.8",
|
||||
"nodemon": "2.0.22",
|
||||
"nyc": "15.1.0",
|
||||
"start-server-and-test": "1.13.1",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"style-loader": "3.3.2",
|
||||
"ts-jest": "27.1.5",
|
||||
"typescript": "5.6.3",
|
||||
|
|
@ -197,8 +199,7 @@
|
|||
"webpack-virtual-modules": "0.4.3",
|
||||
"worker-loader": "3.0.8",
|
||||
"yaml-lint": "1.7.0",
|
||||
"yarn": "1.22.18",
|
||||
"eslint-plugin-local-rules": "link:./eslint-plugin-local-rules"
|
||||
"yarn": "1.22.18"
|
||||
},
|
||||
"resolutions": {
|
||||
"html-webpack-plugin": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ export default defineComponent({
|
|||
v-if="(taints && taints.length) || isView"
|
||||
class="taints"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label class="text-label">
|
||||
|
|
@ -407,6 +408,7 @@ export default defineComponent({
|
|||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<template v-if="taints && taints.length">
|
||||
<Taint
|
||||
v-for="(keyedTaint, i) in taints"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
export default ['1.31',
|
||||
export default ['1.32',
|
||||
|
||||
'1.30',
|
||||
'1.31',
|
||||
|
||||
'1.29',
|
||||
|
||||
'1.28'];
|
||||
'1.30'];
|
||||
|
|
|
|||
|
|
@ -388,17 +388,16 @@ export default defineComponent({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mt-20">
|
||||
<KeyValue
|
||||
:value="tags"
|
||||
:mode="mode"
|
||||
:title="t('eks.tags.label')"
|
||||
:as-map="true"
|
||||
:read-allowed="false"
|
||||
@update:value="$emit('update:tags', $event)"
|
||||
>
|
||||
<template #title>
|
||||
<label class="text-label">{{ t('eks.tags.label') }}</label>
|
||||
<h3 v-t="'eks.tags.label'" />
|
||||
</template>
|
||||
</KeyValue>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ export default defineComponent({
|
|||
</ArrayList>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-10">
|
||||
<div class="row mb-10 mt-20">
|
||||
<div
|
||||
v-if="isNew"
|
||||
class="col span-6"
|
||||
|
|
|
|||
|
|
@ -604,7 +604,7 @@ export default defineComponent({
|
|||
:label="minMaxDesiredErrors"
|
||||
/>
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mt-20">
|
||||
<KeyValue
|
||||
:mode="mode"
|
||||
:title="t('eks.nodeGroups.groupLabels.label')"
|
||||
|
|
@ -614,11 +614,13 @@ export default defineComponent({
|
|||
@update:value="$emit('update:labels', $event)"
|
||||
>
|
||||
<template #title>
|
||||
<label class="text-label">{{ t('eks.nodeGroups.groupLabels.label') }}</label>
|
||||
<h4>
|
||||
{{ t('eks.nodeGroups.groupLabels.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
</KeyValue>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mt-20">
|
||||
<KeyValue
|
||||
:mode="mode"
|
||||
:title="t('eks.nodeGroups.groupTags.label')"
|
||||
|
|
@ -629,7 +631,7 @@ export default defineComponent({
|
|||
@update:value="$emit('update:tags', $event)"
|
||||
>
|
||||
<template #title>
|
||||
<label class="text-label">{{ t('eks.nodeGroups.groupTags.label') }}</label>
|
||||
<h4>{{ t('eks.nodeGroups.groupTags.label') }}</h4>
|
||||
</template>
|
||||
</KeyValue>
|
||||
</div>
|
||||
|
|
@ -812,7 +814,7 @@ export default defineComponent({
|
|||
</div>
|
||||
</div>
|
||||
<div row="mb-10">
|
||||
<div class="col span-12">
|
||||
<div class="col span-12 mt-20">
|
||||
<KeyValue
|
||||
:mode="mode"
|
||||
label-key="eks.nodeGroups.resourceTags.label"
|
||||
|
|
@ -823,7 +825,9 @@ export default defineComponent({
|
|||
@update:value="$emit('update:resourceTags', $event)"
|
||||
>
|
||||
<template #title>
|
||||
<label class="text-label">{{ t('eks.nodeGroups.resourceTags.label') }}</label>
|
||||
<h4>
|
||||
{{ t('eks.nodeGroups.resourceTags.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
</KeyValue>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ harvesterManager:
|
|||
prompt-standard-user: Please contact your system administrator to install the latest Harvester UI Extension, if any
|
||||
missingVersion:
|
||||
warning: "Could not find a compatible version"
|
||||
prompt: "Please update Rancher to get the latest compatible version of the Harvester UI extension"
|
||||
prompt: "Please update Rancher to get the latest compatible version of the Harvester UI extension or try to install it manually"
|
||||
prompt-standard-user: Please contact your system administrator
|
||||
error:
|
||||
warning: "Warning, Harvester UI extension automatic installation failed"
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ import { allHash } from '@shell/utils/promise';
|
|||
import { NAME as APP_PRODUCT } from '@shell/config/product/apps';
|
||||
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
||||
import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
|
||||
import { HARVESTER_CHART, HARVESTER_COMMUNITY_REPO, HARVESTER_RANCHER_REPO } from '../types';
|
||||
import { HARVESTER_CHART, HARVESTER_COMMUNITY_REPO, HARVESTER_RANCHER_REPO, communityRepoRegexes } from '../types';
|
||||
import {
|
||||
getLatestExtensionVersion,
|
||||
getHelmRepository,
|
||||
ensureHelmRepository,
|
||||
getHelmRepositoryExact,
|
||||
getHelmRepositoryMatch,
|
||||
createHelmRepository,
|
||||
refreshHelmRepository,
|
||||
installHelmChart,
|
||||
waitForUIExtension,
|
||||
|
|
@ -102,9 +103,9 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
async harvesterRepository(value) {
|
||||
if (value) {
|
||||
await refreshHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.spec.gitBranch);
|
||||
async harvesterRepository(neu) {
|
||||
if (neu) {
|
||||
await refreshHelmRepository(this.$store, neu.spec.gitRepo || neu.spec.url);
|
||||
|
||||
if (this.harvester.extension) {
|
||||
await this.setHarvesterUpdateVersion();
|
||||
|
|
@ -212,7 +213,11 @@ export default {
|
|||
methods: {
|
||||
async getHarvesterRepository() {
|
||||
try {
|
||||
return await getHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.spec.gitBranch);
|
||||
if (isRancherPrime()) {
|
||||
return await getHelmRepositoryExact(this.$store, HARVESTER_REPO.gitRepo);
|
||||
} else {
|
||||
return await getHelmRepositoryMatch(this.$store, communityRepoRegexes);
|
||||
}
|
||||
} catch (error) {
|
||||
this.harvesterRepositoryError = true;
|
||||
}
|
||||
|
|
@ -234,13 +239,17 @@ export default {
|
|||
let installed = false;
|
||||
|
||||
try {
|
||||
const harvesterRepo = await ensureHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.metadata.name, HARVESTER_REPO.spec.gitBranch);
|
||||
let harvesterRepository = this.harvesterRepository;
|
||||
|
||||
if (!harvesterRepository) {
|
||||
harvesterRepository = await createHelmRepository(this.$store, HARVESTER_REPO.metadata.name, HARVESTER_REPO.gitRepo, HARVESTER_REPO.gitBranch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server issue
|
||||
* It needs to refresh the HelmRepository because the server can have a previous one in the cache.
|
||||
*/
|
||||
await refreshHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.spec.gitBranch);
|
||||
await refreshHelmRepository(this.$store, harvesterRepository.spec.gitRepo || harvesterRepository.spec.url);
|
||||
|
||||
this.harvesterInstallVersion = await getLatestExtensionVersion(this.$store, HARVESTER_CHART.name, this.rancherVersion, this.kubeVersion);
|
||||
|
||||
|
|
@ -250,7 +259,16 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
await installHelmChart(harvesterRepo, { ...HARVESTER_CHART, version: this.harvesterInstallVersion }, {}, UI_PLUGIN_NAMESPACE, 'install');
|
||||
await installHelmChart(
|
||||
harvesterRepository,
|
||||
{
|
||||
...HARVESTER_CHART,
|
||||
version: this.harvesterInstallVersion
|
||||
},
|
||||
{},
|
||||
UI_PLUGIN_NAMESPACE,
|
||||
'install'
|
||||
);
|
||||
|
||||
const extension = await waitForUIExtension(this.$store, HARVESTER_CHART.name);
|
||||
|
||||
|
|
@ -272,7 +290,7 @@ export default {
|
|||
|
||||
try {
|
||||
if (this.harvester.missingRepository) {
|
||||
this.harvesterRepository = await ensureHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.metadata.name, HARVESTER_REPO.spec.gitBranch);
|
||||
this.harvesterRepository = await createHelmRepository(this.$store, HARVESTER_REPO.metadata.name, HARVESTER_REPO.gitRepo, HARVESTER_REPO.gitBranch);
|
||||
|
||||
await this.setHarvesterUpdateVersion();
|
||||
}
|
||||
|
|
@ -283,7 +301,16 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
await installHelmChart(this.harvesterRepository, { ...HARVESTER_CHART, version: this.harvesterUpdateVersion }, {}, UI_PLUGIN_NAMESPACE, 'upgrade');
|
||||
await installHelmChart(
|
||||
this.harvesterRepository,
|
||||
{
|
||||
...HARVESTER_CHART,
|
||||
version: this.harvesterUpdateVersion
|
||||
},
|
||||
{},
|
||||
UI_PLUGIN_NAMESPACE,
|
||||
'upgrade'
|
||||
);
|
||||
|
||||
const extension = await waitForUIExtension(this.$store, HARVESTER_CHART.name);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { UI_PLUGINS_REPOS } from '@shell/config/uiplugins';
|
||||
|
||||
export const communityRepoRegexes = [
|
||||
/^https:\/\/github\.com\/.*\/harvester-ui-extension+/g,
|
||||
/^https:\/\/.*\.github\.io\/harvester-ui-extension+/g,
|
||||
];
|
||||
|
||||
export const HARVESTER_CHART = {
|
||||
name: 'harvester',
|
||||
version: '',
|
||||
|
|
@ -8,21 +13,13 @@ export const HARVESTER_CHART = {
|
|||
};
|
||||
|
||||
export const HARVESTER_COMMUNITY_REPO = {
|
||||
type: 'catalog.cattle.io.clusterrepo',
|
||||
metadata: { name: 'harvester' },
|
||||
spec: {
|
||||
clientSecret: null,
|
||||
gitRepo: 'https://github.com/harvester/harvester-ui-extension',
|
||||
gitBranch: 'gh-pages'
|
||||
}
|
||||
gitBranch: 'gh-pages',
|
||||
};
|
||||
|
||||
export const HARVESTER_RANCHER_REPO = {
|
||||
type: 'catalog.cattle.io.clusterrepo',
|
||||
metadata: { name: 'rancher' },
|
||||
spec: {
|
||||
clientSecret: null,
|
||||
gitRepo: UI_PLUGINS_REPOS.OFFICIAL.URL,
|
||||
gitBranch: UI_PLUGINS_REPOS.OFFICIAL.BRANCH,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./.shell/pkg/babel.config.js');
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<script>
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import { SETTING } from '@shell/config/settings';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { getAllOptionsAfterCurrentVersion, filterOutDeprecatedPatchVersions } from '@shell/utils/cluster';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Basics',
|
||||
components: {
|
||||
LabeledSelect, Checkbox, LabeledInput
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
defaultVersion: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
upgradeStrategy: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
loadingVersions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rules: {
|
||||
default: () => ({
|
||||
workerConcurrency: [],
|
||||
controlPlaneConcurrency: []
|
||||
}),
|
||||
type: Object,
|
||||
},
|
||||
|
||||
},
|
||||
emits: ['kubernetes-version-changed', 'drain-server-nodes-changed', 'server-concurrency-changed',
|
||||
'drain-worker-nodes-changed', 'worker-concurrency-changed', 'enable-authorized-endpoint', 'input'],
|
||||
data() {
|
||||
const store = this.$store;
|
||||
const supportedVersionRange = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_SUPPORTED_K8S_VERSIONS)?.value;
|
||||
const originalVersion = this.config.kubernetesVersion;
|
||||
|
||||
return {
|
||||
supportedVersionRange, originalVersion, showDeprecatedPatchVersions: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
versionOptions() {
|
||||
const cur = this.originalVersion;
|
||||
let out = getAllOptionsAfterCurrentVersion(this.$store, this.versions, cur, this.defaultVersion);
|
||||
|
||||
if (!this.showDeprecatedPatchVersions) {
|
||||
// Normally, we only want to show the most recent patch version
|
||||
// for each Kubernetes minor version. However, if the user
|
||||
// opts in to showing deprecated versions, we don't filter them.
|
||||
out = filterOutDeprecatedPatchVersions(out, cur);
|
||||
}
|
||||
const existing = out.find((x) => x.value === cur);
|
||||
|
||||
if (existing) {
|
||||
existing.disabled = false;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="row row-basics mb-20">
|
||||
<div class="col-basics mr-10 span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="config.kubernetesVersion"
|
||||
data-testid="cruimported-kubernetesversion"
|
||||
:mode="mode"
|
||||
:options="versionOptions"
|
||||
label-key="cluster.kubernetesVersion.label"
|
||||
option-key="value"
|
||||
option-label="label"
|
||||
:loading="loadingVersions"
|
||||
@update:value="$emit('kubernetes-version-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-basics span-6 mt-15">
|
||||
<Checkbox
|
||||
v-model:value="showDeprecatedPatchVersions"
|
||||
:label="t('cluster.kubernetesVersion.deprecatedPatches')"
|
||||
:tooltip="t('cluster.kubernetesVersion.deprecatedPatchWarning')"
|
||||
class="patch-version"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 v-t="'imported.upgradeStrategy.header'" />
|
||||
<div class="col mt-10 mb-10">
|
||||
<div class="col mt-5">
|
||||
<Checkbox
|
||||
:value="upgradeStrategy.drainServerNodes"
|
||||
:mode="mode"
|
||||
:label="t('imported.drainControlPlaneNodes.label')"
|
||||
@update:value="$emit('drain-server-nodes-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col mt-5">
|
||||
<Checkbox
|
||||
:value="upgradeStrategy.drainWorkerNodes"
|
||||
:mode="mode"
|
||||
:label="t('imported.drainWorkerNodes.label')"
|
||||
@update:value="$emit('drain-worker-nodes-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-basics">
|
||||
<div class="col-basics mr-10 span-6">
|
||||
<LabeledInput
|
||||
:value="upgradeStrategy.serverConcurrency"
|
||||
:mode="mode"
|
||||
:label="t('cluster.rke2.controlPlaneConcurrency.label')"
|
||||
:rules="rules.concurrency"
|
||||
required
|
||||
class="mb-10"
|
||||
@update:value="$emit('server-concurrency-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-basics span-6">
|
||||
<LabeledInput
|
||||
:value="upgradeStrategy.workerConcurrency"
|
||||
:mode="mode"
|
||||
:label="t('cluster.rke2.workerConcurrency.label')"
|
||||
:rules="rules.concurrency"
|
||||
required
|
||||
class="mb-10"
|
||||
@update:value="$emit('worker-concurrency-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@media screen and (max-width: 996px) {
|
||||
.row-basics {
|
||||
flex-direction: column;
|
||||
}
|
||||
.col-basics {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
<script>
|
||||
import { set } from '@shell/utils/object';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { defineComponent } from 'vue';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import FormValidation from '@shell/mixins/form-validation';
|
||||
import CruResource from '@shell/components/CruResource.vue';
|
||||
import Loading from '@shell/components/Loading.vue';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import Accordion from '@components/Accordion/Accordion.vue';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
import ClusterMembershipEditor, { canViewClusterMembershipEditor } from '@shell/components/form/Members/ClusterMembershipEditor.vue';
|
||||
import Labels from '@shell/components/form/Labels.vue';
|
||||
import Basics from '@pkg/imported/components/Basics.vue';
|
||||
import ACE from '@shell/edit/provisioning.cattle.io.cluster/tabs/networking/ACE';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CruImported',
|
||||
|
||||
components: {
|
||||
Basics, ACE, Loading, CruResource, KeyValue, LabeledInput, Accordion, Banner, ClusterMembershipEditor, Labels, Checkbox
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, FormValidation],
|
||||
|
||||
props: {
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: _CREATE
|
||||
},
|
||||
|
||||
// provisioning cluster object
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const store = this.$store;
|
||||
|
||||
if (this.value.id) {
|
||||
const liveNormanCluster = await this.value.findNormanCluster();
|
||||
|
||||
this.normanCluster = await store.dispatch(`rancher/clone`, { resource: liveNormanCluster });
|
||||
this.config = this.normanCluster.rke2Config || this.normanCluster.k3sConfig;
|
||||
if ( this.normanCluster && isEmpty(this.normanCluster.localClusterAuthEndpoint) ) {
|
||||
set(this.normanCluster, 'localClusterAuthEndpoint', { enabled: false });
|
||||
}
|
||||
if ( this.normanCluster && !this.normanCluster?.agentEnvVars) {
|
||||
this.normanCluster.agentEnvVars = [];
|
||||
}
|
||||
this.getVersions();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
normanCluster: { name: '' },
|
||||
loadingVersions: false,
|
||||
membershipUpdate: {},
|
||||
config: {},
|
||||
allVersions: [],
|
||||
defaultVer: '',
|
||||
fvFormRuleSets: [{
|
||||
path: 'workerConcurrency',
|
||||
rules: ['workerConcurrencyRule']
|
||||
},
|
||||
{
|
||||
path: 'controlPlaneConcurrency',
|
||||
rules: ['controlPlaneConcurrencyRule']
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings');
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
fvExtraRules() {
|
||||
return {
|
||||
workerConcurrencyRule: () => {
|
||||
const val = this?.normanCluster?.k3sConfig?.k3supgradeStrategy?.workerConcurrency || this?.normanCluster?.rke2Config?.rke2upgradeStrategy?.workerConcurrency || '';
|
||||
const exists = this?.normanCluster?.k3sConfig?.k3supgradeStrategy || this?.normanCluster?.rke2Config?.rke2upgradeStrategy;
|
||||
// BE is only checking that the value is an integer >= 1
|
||||
const valIsInvalid = Number(val) < 1 || !Number.isInteger(+val) || `${ val }`.match(/\.+/g);
|
||||
|
||||
return !!exists && valIsInvalid ? this.t('imported.errors.concurrency', { key: 'Worker Concurrency' }) : undefined ;
|
||||
},
|
||||
controlPlaneConcurrencyRule: () => {
|
||||
const val = this?.normanCluster?.k3sConfig?.k3supgradeStrategy?.serverConcurrency || this?.normanCluster?.rke2Config?.rke2upgradeStrategy?.serverConcurrency || '';
|
||||
const exists = this?.normanCluster?.k3sConfig?.k3supgradeStrategy || this?.normanCluster?.rke2Config?.rke2upgradeStrategy;
|
||||
// BE is only checking that the value is an integer >= 1
|
||||
const valIsInvalid = Number(val) < 1 || !Number.isInteger(+val) || `${ val }`.match(/\.+/g);
|
||||
|
||||
return !!exists && valIsInvalid ? this.t('imported.errors.concurrency', { key: 'Control Plane Concurrency' }) : undefined ;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
upgradeStrategy: {
|
||||
get() {
|
||||
if ( this.normanCluster?.rke2Config ) {
|
||||
return this.normanCluster.rke2Config?.rke2upgradeStrategy;
|
||||
}
|
||||
|
||||
return this.normanCluster?.k3sConfig?.k3supgradeStrategy;
|
||||
},
|
||||
set(newValue) {
|
||||
if ( this.normanCluster?.rke2Config ) {
|
||||
this.normanCluster.rke2Config.rke2upgradeStrategy = newValue;
|
||||
}
|
||||
|
||||
this.normanCluster.k3sConfig.k3supgradeStrategy = newValue;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _CREATE || this.mode === _EDIT;
|
||||
},
|
||||
isK3s() {
|
||||
return !!this.value.isK3s;
|
||||
},
|
||||
isRke2() {
|
||||
return !!this.value.isRke2;
|
||||
},
|
||||
enableNetworkPolicySupported() {
|
||||
// https://github.com/rancher/rancher/pull/33070/files
|
||||
return !this.isK3s && !this.isRke2;
|
||||
},
|
||||
isLocal() {
|
||||
return !!this.value.isLocal;
|
||||
},
|
||||
|
||||
doneRoute() {
|
||||
return this.value?.listLocation?.name;
|
||||
},
|
||||
|
||||
canManageMembers() {
|
||||
return canViewClusterMembershipEditor(this.$store);
|
||||
},
|
||||
|
||||
providerTabKey() {
|
||||
return this.isK3s ? this.t('imported.accordions.k3sOptions') : this.t('imported.accordions.rke2Options');
|
||||
},
|
||||
// If the cluster hasn't been fully imported yet, we won't have this information yet
|
||||
// and Basics should be hidden
|
||||
showBasics() {
|
||||
return !!this.config;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onMembershipUpdate(update) {
|
||||
this.membershipUpdate = update;
|
||||
},
|
||||
async saveRoleBindings() {
|
||||
if (this.membershipUpdate.save) {
|
||||
await this.membershipUpdate.save(this.normanCluster.id);
|
||||
}
|
||||
},
|
||||
async actuallySave() {
|
||||
return await this.normanCluster.save();
|
||||
},
|
||||
|
||||
async getVersions() {
|
||||
this.loadingVersions = true;
|
||||
this.versionOptions = [];
|
||||
|
||||
try {
|
||||
const globalSettings = await this.$store.getters['management/all'](MANAGEMENT.SETTING) || [];
|
||||
let hash = {};
|
||||
|
||||
if (this.isK3s) {
|
||||
hash = { versions: this.$store.dispatch('management/request', { url: '/v1-k3s-release/releases' }) };
|
||||
|
||||
const defaultK3sSetting = globalSettings.find((setting) => setting.id === 'k3s-default-version') || {};
|
||||
|
||||
this.defaultVersion = defaultK3sSetting?.value || defaultK3sSetting?.default;
|
||||
|
||||
// Use the channel if we can not get the version from the settings
|
||||
if (!this.defaultVersion) {
|
||||
hash.channels = this.$store.dispatch('management/request', { url: '/v1-k3s-release/channels' });
|
||||
}
|
||||
} else {
|
||||
hash = { versions: this.$store.dispatch('management/request', { url: '/v1-rke2-release/releases' }) };
|
||||
|
||||
const defaultRke2Setting = globalSettings.find((setting) => setting.id === 'rke2-default-version') || {};
|
||||
|
||||
this.defaultVersion = defaultRke2Setting?.value || defaultRke2Setting?.default;
|
||||
|
||||
if (!this.defaultVersion) {
|
||||
hash.channels = this.$store.dispatch('management/request', { url: '/v1-rke2-release/channels' });
|
||||
}
|
||||
}
|
||||
const res = await allHash(hash);
|
||||
|
||||
this.allVersions = res.versions?.data || [];
|
||||
if (!this.defaultVersion) {
|
||||
const channels = res.channels?.data || [];
|
||||
|
||||
this.defaultVersion = channels.find((x) => x.id === 'default')?.latest;
|
||||
}
|
||||
|
||||
this.loadingVersions = false;
|
||||
} catch (err) {
|
||||
this.loadingVersions = false;
|
||||
const errors = this.errors;
|
||||
|
||||
errors.push(this.t('imported.errors.kubernetesVersions', { e: err.error || err }));
|
||||
}
|
||||
},
|
||||
|
||||
kubernetesVersionChanged(val) {
|
||||
if ( !this.isK3s ) {
|
||||
this.normanCluster.rke2Config.kubernetesVersion = val;
|
||||
} else {
|
||||
this.normanCluster.k3sConfig.kubernetesVersion = val;
|
||||
}
|
||||
},
|
||||
enableLocalClusterAuthEndpoint(neu) {
|
||||
this.normanCluster.localClusterAuthEndpoint.enabled = neu;
|
||||
if (!!neu) {
|
||||
this.normanCluster.localClusterAuthEndpoint.caCerts = '';
|
||||
this.normanCluster.localClusterAuthEndpoint.fqdn = '';
|
||||
} else {
|
||||
delete this.normanCluster.localClusterAuthEndpoint.caCerts;
|
||||
delete this.normanCluster.localClusterAuthEndpoint.fqdn;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:can-yaml="false"
|
||||
:done-route="doneRoute"
|
||||
:errors="fvUnreportedValidationErrors"
|
||||
:validation-passed="fvFormIsValid"
|
||||
@error="e=>errors=e"
|
||||
@finish="save"
|
||||
>
|
||||
<Loading
|
||||
v-if="$fetchState.pending"
|
||||
mode="relative"
|
||||
/>
|
||||
<div v-else>
|
||||
<div class="mt-10">
|
||||
<div class="row mb-10">
|
||||
<div class="col span-3">
|
||||
<LabeledInput
|
||||
v-model:value="normanCluster.name"
|
||||
:mode="mode"
|
||||
:disabled="true"
|
||||
label-key="generic.name"
|
||||
data-testid="imported-name"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isLocal"
|
||||
class="col span-3"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="normanCluster.description"
|
||||
:mode="mode"
|
||||
label-key="nameNsDescription.description.label"
|
||||
:placeholder="t('nameNsDescription.description.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Accordion
|
||||
v-if="showBasics"
|
||||
:title="providerTabKey"
|
||||
:open-initially="true"
|
||||
class="mb-20"
|
||||
>
|
||||
<Basics
|
||||
:value="normanCluster"
|
||||
:mode="mode"
|
||||
:config="config"
|
||||
:upgrade-strategy="upgradeStrategy"
|
||||
:versions="allVersions"
|
||||
:default-version="defaultVersion"
|
||||
:loading-versions="loadingVersions"
|
||||
:rules="{workerConcurrency: fvGetAndReportPathRules('workerConcurrency'), controlPlaneConcurrency: fvGetAndReportPathRules('controlPlaneConcurrency') }"
|
||||
@kubernetes-version-changed="kubernetesVersionChanged"
|
||||
@drain-server-nodes-changed="(val)=>upgradeStrategy.drainServerNodes = val"
|
||||
@drain-worker-nodes-changed="(val)=>upgradeStrategy.drainWorkerNodes = val"
|
||||
@server-concurrency-changed="(val)=>upgradeStrategy.serverConcurrency = val"
|
||||
@worker-concurrency-changed="(val)=>upgradeStrategy.workerConcurrency = val"
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
class="mb-20"
|
||||
title-key="imported.accordions.clusterMembers"
|
||||
:open-initially="true"
|
||||
>
|
||||
<Banner
|
||||
v-if="isEdit"
|
||||
color="info"
|
||||
>
|
||||
{{ t('cluster.memberRoles.removeMessage') }}
|
||||
</Banner>
|
||||
<ClusterMembershipEditor
|
||||
v-if="canManageMembers"
|
||||
:mode="mode"
|
||||
:parent-id="normanCluster.id ? normanCluster.id : null"
|
||||
@membership-update="onMembershipUpdate"
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
class="mb-20"
|
||||
title-key="imported.accordions.labels"
|
||||
:open-initially="true"
|
||||
>
|
||||
<Labels
|
||||
v-model:value="normanCluster"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
class="mb-20"
|
||||
title-key="imported.accordions.networking"
|
||||
:open-initially="true"
|
||||
>
|
||||
<div
|
||||
v-if="enableNetworkPolicySupported"
|
||||
class="mb-20"
|
||||
>
|
||||
<Banner
|
||||
v-if="!!normanCluster.enableNetworkPolicy"
|
||||
color="info"
|
||||
label-key="imported.network.banner"
|
||||
/>
|
||||
<Checkbox
|
||||
v-model:value="normanCluster.enableNetworkPolicy"
|
||||
:mode="mode"
|
||||
:label="t('cluster.rke2.enableNetworkPolicy.label')"
|
||||
/>
|
||||
</div>
|
||||
<h3 v-t="'cluster.tabs.ace'" />
|
||||
<ACE
|
||||
v-model:value="normanCluster.localClusterAuthEndpoint"
|
||||
:mode="mode"
|
||||
@local-cluster-auth-endpoint-changed="enableLocalClusterAuthEndpoint"
|
||||
@ca-certs-changed="(val)=>normanCluster.localClusterAuthEndpoint.caCerts = val"
|
||||
@fqdn-changed="(val)=>normanCluster.localClusterAuthEndpoint.fqdn = val"
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
class="mb-20"
|
||||
title-key="imported.accordions.advanced"
|
||||
:open-initially="false"
|
||||
>
|
||||
<h3>
|
||||
{{ t('imported.agentEnv.header') }}
|
||||
</h3>
|
||||
<KeyValue
|
||||
v-model:value="normanCluster.agentEnvVars"
|
||||
:mode="mode"
|
||||
key-name="name"
|
||||
:as-map="false"
|
||||
:preserve-keys="['valueFrom']"
|
||||
:supported="(row) => typeof row.valueFrom === 'undefined'"
|
||||
:read-allowed="true"
|
||||
:value-can-be-empty="true"
|
||||
:key-label="t('cluster.agentEnvVars.keyLabel')"
|
||||
:parse-lines-from-file="true"
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { importTypes } from '@rancher/auto-import';
|
||||
import { IPlugin } from '@shell/core/types';
|
||||
import { ImportedProvisioner, LocalProvisioner } from './provisioner';
|
||||
|
||||
// Init the package
|
||||
export default function(plugin: IPlugin): void {
|
||||
// Auto-import model, detail, edit from the folders
|
||||
importTypes(plugin);
|
||||
|
||||
// Provide plugin metadata from package.json
|
||||
plugin.metadata = require('./package.json');
|
||||
|
||||
// Register custom provisioner object
|
||||
plugin.register('provisioner', ImportedProvisioner.ID, ImportedProvisioner);
|
||||
plugin.register('provisioner', LocalProvisioner.ID, LocalProvisioner);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
imported:
|
||||
label: Imported
|
||||
agentEnv:
|
||||
header: Agent Environment Variables
|
||||
drainWorkerNodes:
|
||||
label: Drain Worker Nodes
|
||||
drainControlPlaneNodes:
|
||||
label: Drain Control Plane Nodes
|
||||
network:
|
||||
banner: The imported cluster must support Kubernetes NetworkPolicy resources for Project Network Isolation to be enforced
|
||||
accordions:
|
||||
advanced: Advanced
|
||||
basics: Basics
|
||||
upgrade: Upgrade Strategy
|
||||
networking: Networking
|
||||
clusterMembers: Cluster Members
|
||||
labels: Labels and Annotations
|
||||
k3sOptions: K3S Options
|
||||
rke2Options: RKE2 Options
|
||||
upgradeStrategy:
|
||||
header: Upgrade Strategy
|
||||
errors:
|
||||
concurrency: '{key} must be an integer greater than 0'
|
||||
kubernetesVersions: 'An error occured while fetching the available kubernetes versions: {e}'
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "imported",
|
||||
"description": "Edit imported clusters in Rancher",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
"catalog.cattle.io/display-name": "Edit imported cluster"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "./node_modules/.bin/nuxt dev",
|
||||
"nuxt": "./node_modules/.bin/nuxt"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { IClusterProvisioner } from '@shell/core/types';
|
||||
import CruImported from './components/CruImported.vue';
|
||||
import { Component } from 'vue';
|
||||
|
||||
export class ImportedProvisioner implements IClusterProvisioner {
|
||||
static ID = 'imported'
|
||||
|
||||
get id(): string {
|
||||
return ImportedProvisioner.ID;
|
||||
}
|
||||
|
||||
get component(): Component {
|
||||
return CruImported;
|
||||
}
|
||||
|
||||
get detailTabs(): any {
|
||||
return {};
|
||||
}
|
||||
|
||||
get hidden(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
return 'kontainer';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalProvisioner implements IClusterProvisioner {
|
||||
static ID = 'local'
|
||||
|
||||
get id(): string {
|
||||
return LocalProvisioner.ID;
|
||||
}
|
||||
|
||||
get component(): Component {
|
||||
return CruImported;
|
||||
}
|
||||
|
||||
get detailTabs(): any {
|
||||
return {};
|
||||
}
|
||||
|
||||
get hidden(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
return 'kontainer';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"preserveSymlinks": true,
|
||||
"typeRoots": [
|
||||
"../../node_modules",
|
||||
"../../shell/types",
|
||||
"./types"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"webpack-env",
|
||||
"@types/node",
|
||||
"@types/jest",
|
||||
"@types/lodash",
|
||||
"rancher",
|
||||
"shell"
|
||||
],
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
],
|
||||
"paths": {
|
||||
"@shell/*": [
|
||||
"../../shell/*"
|
||||
],
|
||||
"@components/*": [
|
||||
"@rancher/components/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.d.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"../../node_modules"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./.shell/pkg/vue.config')(__dirname);
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"babel-eslint": "10.1.0",
|
||||
"core-js": "3.40.0",
|
||||
"cron-validator": "1.3.1",
|
||||
"cronstrue": "2.50.0",
|
||||
"cronstrue": "2.53.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "5.2.0",
|
||||
|
|
|
|||
|
|
@ -47,12 +47,12 @@ export default defineComponent({
|
|||
data-testid="accordion-chevron"
|
||||
/>
|
||||
<slot name="header">
|
||||
<h4
|
||||
<h2
|
||||
data-testid="accordion-title-slot-content"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ titleKey ? t(titleKey) : title }}
|
||||
</h4>
|
||||
</h2>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -70,7 +70,7 @@ export default defineComponent({
|
|||
border: 1px solid var(--border)
|
||||
}
|
||||
.accordion-header {
|
||||
padding: 5px;
|
||||
padding: 16px 16px 16px 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&>*{
|
||||
|
|
@ -81,6 +81,6 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
.accordion-body {
|
||||
padding: 10px;
|
||||
padding: 0px 16px 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,13 @@ export default defineComponent({
|
|||
background: transparent;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
// Added badge-disabled instead of bg-disabled since bg-disabled is used in other places with !important styling, an investigation is needed to make the naming consistent
|
||||
&.badge-disabled {
|
||||
color: var(--badge-state-disabled-text);
|
||||
background-color: var( --badge-state-disabled-bg);
|
||||
border: 1px solid var(--badge-state-disabled-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { createFocusTrap, FocusTrap } from 'focus-trap';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Card',
|
||||
|
|
@ -50,12 +51,37 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
triggerFocusTrap: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { focusTrapInstance: {} as FocusTrap };
|
||||
},
|
||||
mounted() {
|
||||
if (this.triggerFocusTrap) {
|
||||
this.focusTrapInstance = createFocusTrap(this.$refs.cardContainer as HTMLElement, {
|
||||
escapeDeactivates: true,
|
||||
allowOutsideClick: true,
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.focusTrapInstance.activate();
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.focusTrapInstance && this.triggerFocusTrap) {
|
||||
this.focusTrapInstance.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="cardContainer"
|
||||
class="card-container"
|
||||
:class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
|
||||
data-testid="card"
|
||||
|
|
|
|||
|
|
@ -264,13 +264,15 @@ export default defineComponent({
|
|||
<template v-else-if="label">{{ label }}</template>
|
||||
<i
|
||||
v-if="tooltipKey"
|
||||
v-clean-tooltip="t(tooltipKey)"
|
||||
v-clean-tooltip="{content: t(tooltipKey), triggers: ['hover', 'touch', 'focus']}"
|
||||
class="checkbox-info icon icon-info icon-lg"
|
||||
:tabindex="isDisabled ? -1 : 0"
|
||||
/>
|
||||
<i
|
||||
v-else-if="tooltip"
|
||||
v-clean-tooltip="tooltip"
|
||||
v-clean-tooltip="{content: tooltip, triggers: ['hover', 'touch', 'focus']}"
|
||||
class="checkbox-info icon icon-info icon-lg"
|
||||
:tabindex="isDisabled ? -1 : 0"
|
||||
/>
|
||||
</slot>
|
||||
</span>
|
||||
|
|
@ -329,6 +331,11 @@ $fontColor: var(--input-label);
|
|||
.checkbox-info {
|
||||
line-height: normal;
|
||||
margin-left: 2px;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-custom {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
|
|||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
||||
});
|
||||
|
||||
it('using mode "multiline" should emit input value correctly', () => {
|
||||
it('using type "multiline" should emit input value correctly', () => {
|
||||
const value = 'any-string';
|
||||
const delay = 1;
|
||||
const wrapper = mount(LabeledInput, {
|
||||
|
|
@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
|
|||
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
||||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
||||
});
|
||||
|
||||
describe('using type "chron"', () => {
|
||||
it.each([
|
||||
['0 * * * *', 'Every hour, every day'],
|
||||
['@daily', 'At 12:00 AM, every day'],
|
||||
['You must fail! Go!', '%generic.invalidCron%'],
|
||||
])('passing value %p should display hint %p', (value, hint) => {
|
||||
const wrapper = mount(LabeledInput, {
|
||||
propsData: { value, type: 'cron' },
|
||||
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
|
||||
});
|
||||
|
||||
const subLabel = wrapper.find('[data-testid="sub-label"]');
|
||||
|
||||
expect(subLabel.text()).toBe(hint);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -179,14 +179,28 @@ export default defineComponent({
|
|||
if (this.type !== 'cron' || !this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO - #13202: This is required due use of 2 libraries and 3 different libraries through the code.
|
||||
const predefined = [
|
||||
'@yearly',
|
||||
'@annually',
|
||||
'@monthly',
|
||||
'@weekly',
|
||||
'@daily',
|
||||
'@midnight',
|
||||
'@hourly'
|
||||
];
|
||||
const isPredefined = predefined.includes(this.value as string);
|
||||
|
||||
// refer https://github.com/GuillaumeRochat/cron-validator#readme
|
||||
if (!isValidCron(this.value as string, {
|
||||
if (!isPredefined && !isValidCron(this.value as string, {
|
||||
alias: true,
|
||||
allowBlankDay: true,
|
||||
allowSevenAsSunday: true,
|
||||
})) {
|
||||
return this.t('generic.invalidCron');
|
||||
}
|
||||
|
||||
try {
|
||||
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
|
||||
|
||||
|
|
@ -382,6 +396,7 @@ export default defineComponent({
|
|||
<div
|
||||
v-if="cronHint || subLabel"
|
||||
class="sub-label"
|
||||
data-testid="sub-label"
|
||||
>
|
||||
<div
|
||||
v-if="cronHint"
|
||||
|
|
|
|||
|
|
@ -243,14 +243,8 @@ $fontColor: var(--input-label);
|
|||
min-width: 14px;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease-out;
|
||||
border: 1.5px solid var(--border);
|
||||
margin-top: 5px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ export default defineComponent({
|
|||
*/
|
||||
isDisabled(): boolean {
|
||||
return (this.disabled || this.isView);
|
||||
},
|
||||
radioGroupLabel(): string {
|
||||
return this.labelKey ? this.t(this.labelKey) : this.label ? this.label : '';
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -202,9 +205,10 @@ export default defineComponent({
|
|||
|
||||
<!-- Group -->
|
||||
<div
|
||||
role="radiogroup"
|
||||
:aria-label="radioGroupLabel"
|
||||
class="radio-group"
|
||||
:class="{'row':row}"
|
||||
tabindex="0"
|
||||
@keyup.down.stop="clickNext(1)"
|
||||
@keyup.up.stop="clickNext(-1)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -62,12 +62,30 @@ INPUT,
|
|||
SELECT,
|
||||
TEXTAREA,
|
||||
.labeled-input,
|
||||
.labeled-select,
|
||||
.unlabeled-select,
|
||||
.checkbox-custom,
|
||||
.radio-custom {
|
||||
.checkbox-custom {
|
||||
&:focus, &.focused {
|
||||
@include form-focus
|
||||
@include form-focus;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-custom,
|
||||
.labeled-select,
|
||||
.unlabeled-select {
|
||||
&:focus-visible, &.focused {
|
||||
@include focus-outline;
|
||||
}
|
||||
}
|
||||
|
||||
.labeled-select,
|
||||
.unlabeled-select {
|
||||
&.focused {
|
||||
border-color: var(--outline);
|
||||
}
|
||||
}
|
||||
|
||||
.unlabeled-select {
|
||||
&:focus-visible, &.focused {
|
||||
@include focus-outline;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,5 +158,6 @@
|
|||
|
||||
@mixin focus-outline {
|
||||
// Focus for form like elements (not to be confused with basic :focus style)
|
||||
// we need to use !important because it needs to superseed other classes that might impact outlines
|
||||
outline: 2px solid var(--primary-keyboard-focus);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,11 @@ button,
|
|||
color: var(--link);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.role-multi-action {
|
||||
|
|
@ -185,6 +190,11 @@ fieldset[disabled] .btn {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
z-index: 1;
|
||||
@include focus-outline;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@extend .bg-primary;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ TEXTAREA,
|
|||
|
||||
@include input-status-color;
|
||||
|
||||
&:focus, &.focused {
|
||||
@include form-focus
|
||||
&:focus:not(.unlabeled-select):not(.labeled-select), &.focused:not(.unlabeled-select):not(.labeled-select) {
|
||||
@include form-focus;
|
||||
}
|
||||
|
||||
LABEL {
|
||||
|
|
|
|||
|
|
@ -219,4 +219,8 @@
|
|||
|
||||
--product-icon : #{$lighter};
|
||||
--product-icon-active : #{$lightest};
|
||||
|
||||
--badge-state-disabled-text : #{$desc-light};
|
||||
--badge-state-disabled-bg : #{$medium};
|
||||
--badge-state-disabled-border: #{$lighter};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,4 +544,8 @@ BODY, .theme-light {
|
|||
|
||||
--product-icon : #{$darker};
|
||||
--product-icon-active : #{$darkest};
|
||||
|
||||
--badge-state-disabled-text : #{$darker};
|
||||
--badge-state-disabled-bg : #{$lighter};
|
||||
--badge-state-disabled-border: #{$medium};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@
|
|||
color: var(--dropdown-text);
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
// needs overflow-x hidden so that select for type of charts in cluster chart page
|
||||
// doesn't show a horizontal scroll https://github.com/rancher/dashboard/pull/13139/files/9e6a8e64181de4525ad378d4d1b93c8efc2ed6d5#r1929296126
|
||||
overflow-x: hidden;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ generic:
|
|||
back: Back
|
||||
cancel: Cancel
|
||||
confirm: Confirm
|
||||
colorPicker: Color picker
|
||||
clear: Clear
|
||||
clearAll: Clear All
|
||||
close: Close
|
||||
|
|
@ -134,6 +135,8 @@ nav:
|
|||
support: Support page link
|
||||
about: About page link
|
||||
pinCluster: Pin/Unpin cluster
|
||||
collapseExpand: Collapse/Expand menu group
|
||||
productAboutPage: Product about page link
|
||||
alt:
|
||||
mainMenuIcon: Main menu icon
|
||||
mainMenuRancherLogo: Main menu Rancher logo
|
||||
|
|
@ -514,6 +517,9 @@ authConfig:
|
|||
starttls:
|
||||
label: Start TLS
|
||||
tip: Upgrades non-encrypted connections by wrapping with TLS during the connection process. Can not be used in conjunction with TLS.
|
||||
searchUsingServiceAccount:
|
||||
label: Enable Service Account Search
|
||||
tip: When enabled, Rancher will use the service account instead of the user account to search for users and groups.
|
||||
tls: TLS
|
||||
userEnabledAttribute: User Enabled Attribute
|
||||
userMemberAttribute: User Member Attribute
|
||||
|
|
@ -933,6 +939,7 @@ catalog:
|
|||
experimentalWarning: '{chartName} has been marked as experimental. Use caution when installing this helm chart as it might not function as expected.'
|
||||
deprecatedAndExperimentalWarning: '{chartName} has been marked as deprecated and experimental. Use caution when installing this helm chart as it might be removed in the future and might not function as expected.'
|
||||
charts:
|
||||
refresh: refresh charts
|
||||
all: All
|
||||
categories:
|
||||
all: All Categories
|
||||
|
|
@ -952,7 +959,7 @@ catalog:
|
|||
all: All Operating Systems
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
search: Filter
|
||||
search: Filter charts results
|
||||
deprecatedChartsFilter:
|
||||
label: Show deprecated apps
|
||||
install:
|
||||
|
|
@ -963,7 +970,7 @@ catalog:
|
|||
chart: Chart
|
||||
warning:
|
||||
managed:
|
||||
Warning, Rancher manages deployment and upgrade of the {name} app. Upgrading this app is not supported.<br>
|
||||
Warning, {manager} manages deployment and upgrade of the {name} app. Upgrading this app is not supported.<br>
|
||||
{version, select,
|
||||
null { }
|
||||
other { Under most circumstances, no user intervention should be needed to ensure that the {version} version is compatible with the version of Rancher that you are running.}
|
||||
|
|
@ -1063,7 +1070,7 @@ catalog:
|
|||
windows: '{ver} (Windows-only)'
|
||||
delete:
|
||||
warning:
|
||||
managed: Warning, Rancher manages deployment and upgrade of the {name} app. Deleting this app is not supported.
|
||||
managed: Warning, {manager} manages deployment and upgrade of the {name} app. Deleting this app is not supported.
|
||||
operation:
|
||||
tableHeaders:
|
||||
action: Action
|
||||
|
|
@ -2076,6 +2083,7 @@ cluster:
|
|||
label: Control Plane Concurrency
|
||||
toolTip: "This can be either a fixed number of nodes (e.g. 1) at a time or a percentage (e.g. 10%)"
|
||||
workerConcurrency:
|
||||
header: Worker Nodes
|
||||
label: Worker Concurrency
|
||||
toolTip: "This can be either a fixed number of nodes (e.g. 1) at a time or a percentage (e.g. 10%)"
|
||||
drain:
|
||||
|
|
@ -2111,6 +2119,7 @@ cluster:
|
|||
tlsSan:
|
||||
label: TLS Alternate Names
|
||||
fqdn:
|
||||
label: FQDN
|
||||
toolTip: A FQDN which will resolve to the healthy control plane nodes of the cluster.
|
||||
caCerts:
|
||||
label: CA Certificates
|
||||
|
|
@ -2383,6 +2392,9 @@ fleet:
|
|||
cluster:
|
||||
summary: Resource Summary
|
||||
nonReady: Non-Ready Bundles
|
||||
labels: Labels
|
||||
hideLabels: Show less
|
||||
showLabels: Show more
|
||||
clusters:
|
||||
harvester: |-
|
||||
There {count, plural,
|
||||
|
|
@ -6032,6 +6044,8 @@ validation:
|
|||
flowOutput:
|
||||
both: Requires "Output" or "Cluster Output" to be selected.
|
||||
global: Requires "Cluster Output" to be selected.
|
||||
git:
|
||||
repository: Repository URL must be a HTTP(s) or SSH url with no trailing spaces
|
||||
output:
|
||||
logdna:
|
||||
apiKey: Required an "Api Key" to be set.
|
||||
|
|
@ -6135,6 +6149,11 @@ validation:
|
|||
interval: '"{key}" must be of a format with digits followed by a unit i.e. 1h, 2m, 30s'
|
||||
tab: "One or more fields in this tab contain a form validation error"
|
||||
|
||||
carousel:
|
||||
previous: Previous
|
||||
next: Next
|
||||
controlItem: Go to slide nº {number}
|
||||
|
||||
wizard:
|
||||
previous: Previous
|
||||
finish: Finish
|
||||
|
|
@ -6178,7 +6197,7 @@ wm:
|
|||
containerShell:
|
||||
clear: Clear
|
||||
containerName: "Container: {label}"
|
||||
failed: "Unable to open a shell to the container (none of the shell commmands succeeded)\n\r"
|
||||
failed: "Unable to open a shell to the container (none of the shell commands succeeded)\n\r"
|
||||
logLevel:
|
||||
info: INFO
|
||||
error: ERROR
|
||||
|
|
@ -6191,6 +6210,11 @@ wm:
|
|||
title: "Kubectl: {name}"
|
||||
|
||||
workload:
|
||||
scaleWorkloads: Scale workloads
|
||||
healthWorkloads: Jobs/Pods health status
|
||||
healthScaleToggle: Toggle/Expand workloads scaling/health
|
||||
plus: Scale up workload
|
||||
minus: Scale down workload
|
||||
container:
|
||||
command:
|
||||
addEnvVar: Add Variable
|
||||
|
|
@ -7434,6 +7458,8 @@ registryConfig:
|
|||
##############################
|
||||
|
||||
advancedSettings:
|
||||
setEnv: Set by Environment Variable
|
||||
hideShow: Hide/show setting
|
||||
label: Settings
|
||||
subtext: Typical users will not need to change these. Proceed with caution, incorrect values can break your {appName} installation. Settings which have been customized from default settings are tagged 'Modified'.
|
||||
show: Show
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ export default {
|
|||
type="button"
|
||||
:class="opt.class"
|
||||
:disabled="disabled || opt.disabled"
|
||||
role="button"
|
||||
:aria-label="opt.labelKey ? t(opt.labelKey) : opt.label"
|
||||
@click="change(opt.value)"
|
||||
>
|
||||
<slot
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, defineEmits } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,36 @@ export default {
|
|||
</div>
|
||||
</component>
|
||||
</div>
|
||||
<div
|
||||
ref="prev"
|
||||
role="button"
|
||||
class="prev"
|
||||
:aria-label="t('carousel.previous')"
|
||||
:aria-disabled="sliders.length === 1"
|
||||
:class="{'disable': sliders.length === 1}"
|
||||
tabindex="0"
|
||||
@click="nextPrev('prev')"
|
||||
@keyup.enter.space="nextPrev('prev')"
|
||||
>
|
||||
<i
|
||||
class="icon icon-chevron-left icon-4x"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="next"
|
||||
role="button"
|
||||
class="next"
|
||||
:aria-label="t('carousel.next')"
|
||||
:aria-disabled="sliders.length === 1"
|
||||
:class="{'disable': sliders.length === 1}"
|
||||
tabindex="0"
|
||||
@click="nextPrev('next')"
|
||||
@keyup.enter.space="nextPrev('next')"
|
||||
>
|
||||
<i
|
||||
class="icon icon-chevron-right icon-4x"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="controls"
|
||||
:class="{'disable': sliders.length === 1}"
|
||||
|
|
@ -204,25 +234,13 @@ export default {
|
|||
:key="i"
|
||||
class="control-item"
|
||||
:class="{'active': activeItemId === i}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="t('carousel.controlItem', { number: i+1 })"
|
||||
@click="scrollSlide(i, slider.length)"
|
||||
@keyup.enter.space="scrollSlide(i, slider.length)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="prev"
|
||||
class="prev"
|
||||
:class="{'disable': sliders.length === 1}"
|
||||
@click="nextPrev('prev')"
|
||||
>
|
||||
<i class="icon icon-chevron-left icon-4x" />
|
||||
</div>
|
||||
<div
|
||||
ref="next"
|
||||
class="next"
|
||||
:class="{'disable': sliders.length === 1}"
|
||||
@click="nextPrev('next')"
|
||||
>
|
||||
<i class="icon icon-chevron-right icon-4x" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -240,20 +258,6 @@ export default {
|
|||
&.disable::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.disable:hover {
|
||||
.prev,
|
||||
.next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.prev,
|
||||
.next {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-track {
|
||||
|
|
@ -367,13 +371,16 @@ export default {
|
|||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 90px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled .icon {
|
||||
color: var(--disabled-bg);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon:focus-visible {
|
||||
@include focus-outline;
|
||||
}
|
||||
}
|
||||
|
||||
.next {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ export default {
|
|||
mode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -89,7 +93,10 @@ export default {
|
|||
class="locale-chooser"
|
||||
>
|
||||
{{ selectedLocaleLabel }}
|
||||
<i class="icon icon-fw icon-sort-down" />
|
||||
<i
|
||||
v-if="showIcon"
|
||||
class="icon icon-fw icon-sort-down"
|
||||
/>
|
||||
</a>
|
||||
<template #popper>
|
||||
<ul
|
||||
|
|
|
|||
|
|
@ -2,13 +2,6 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import ResourceFetch from '@shell/mixins/resource-fetch';
|
||||
import ResourceTable from '@shell/components/ResourceTable.vue';
|
||||
import { StorePaginationResult } from '@shell/types/store/pagination.types';
|
||||
|
||||
export type FetchSecondaryResourcesOpts = { canPaginate: boolean }
|
||||
export type FetchSecondaryResources = (opts: FetchSecondaryResourcesOpts) => Promise<any>
|
||||
|
||||
export type FetchPageSecondaryResourcesOpts = { canPaginate: boolean, force: boolean, page: any[], pagResult: StorePaginationResult }
|
||||
export type FetchPageSecondaryResources = (opts: FetchPageSecondaryResourcesOpts) => Promise<any>
|
||||
|
||||
/**
|
||||
* This is meant to enable ResourceList like capabilities outside of List pages / components
|
||||
|
|
@ -57,6 +50,8 @@ export default defineComponent({
|
|||
* Information may be required from resources other than the primary one shown per row
|
||||
*
|
||||
* This will fetch them ALL and will be run in a non-server-side pagination world
|
||||
*
|
||||
* of type PagTableFetchSecondaryResources
|
||||
*/
|
||||
fetchSecondaryResources: {
|
||||
type: Function,
|
||||
|
|
@ -69,6 +64,8 @@ export default defineComponent({
|
|||
* This will fetch only those relevant to the current page using server-side pagination based filters
|
||||
*
|
||||
* called from shell/mixins/resource-fetch-api-pagination.js
|
||||
*
|
||||
* of type PagTableFetchPageSecondaryResources
|
||||
*/
|
||||
fetchPageSecondaryResources: {
|
||||
type: Function,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,19 @@ export default {
|
|||
|
||||
return out.filter((obj) => obj.percent);
|
||||
},
|
||||
ariaLabelText() {
|
||||
if (Array.isArray(this.values) && this.values.length) {
|
||||
let ariaLabel = '';
|
||||
|
||||
this.values.forEach((val) => {
|
||||
ariaLabel += `${ val.value } ${ val.value === 1 ? 'item' : 'items' } ${ val.label }`;
|
||||
});
|
||||
|
||||
return ariaLabel;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -108,6 +121,7 @@ function toPercent(value, min, max) {
|
|||
<div
|
||||
v-trim-whitespace
|
||||
:class="{progress: true, multi: pieces.length > 1}"
|
||||
:aria-label="ariaLabelText"
|
||||
>
|
||||
<div
|
||||
v-for="(piece, idx) of pieces"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { filterBy } from '@shell/utils/array';
|
||||
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
|
||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||
import Question from './Question';
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ const LEGACY_MAP = {
|
|||
export default {
|
||||
emits: ['update:value'],
|
||||
|
||||
components: { LabeledInput, LabeledSelect },
|
||||
components: { LabeledInput, ResourceLabeledSelect },
|
||||
mixins: [Question],
|
||||
|
||||
props: {
|
||||
|
|
@ -29,12 +30,6 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
if ( this.typeSchema ) {
|
||||
this.all = await this.$store.dispatch(`${ this.inStore }/findAll`, { type: this.typeName });
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const t = this.question.type;
|
||||
|
||||
|
|
@ -58,28 +53,60 @@ export default {
|
|||
typeName,
|
||||
typeSchema,
|
||||
all: [],
|
||||
allResourceSettings: {
|
||||
updateResources: (all) => {
|
||||
// Filter to only include required namespaced resources
|
||||
const resources = this.isNamespaced ? all.filter((r) => r.metadata.namespace === this.targetNamespace) : all;
|
||||
|
||||
return this.mapResourcesToOptions(resources);
|
||||
}
|
||||
},
|
||||
paginateResourceSetting: {
|
||||
updateResources: (resources) => {
|
||||
return this.mapResourcesToOptions(resources);
|
||||
},
|
||||
/**
|
||||
* of type PaginateTypeOverridesFn
|
||||
* @param [LabelSelectPaginationFunctionOptions] opts
|
||||
* @returns LabelSelectPaginationFunctionOptions
|
||||
*/
|
||||
requestSettings: (opts) => {
|
||||
// Filter to only include required namespaced resources
|
||||
const filters = this.isNamespaced ? [
|
||||
PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: this.targetNamespace }),
|
||||
] : [];
|
||||
|
||||
return {
|
||||
...opts,
|
||||
filters,
|
||||
groupByNamespace: false,
|
||||
classify: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
mapResourcesToOptions(resources) {
|
||||
return resources.map((r) => {
|
||||
if (r.id) {
|
||||
return {
|
||||
label: r.nameDisplay || r.metadata.name,
|
||||
value: r.metadata.name
|
||||
};
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
isNamespaced() {
|
||||
return !!this.typeSchema?.attributes?.namespaced;
|
||||
},
|
||||
|
||||
options() {
|
||||
let out = this.all;
|
||||
|
||||
if ( this.isNamespaced ) {
|
||||
out = filterBy(this.all, 'metadata.namespace', this.targetNamespace);
|
||||
}
|
||||
|
||||
return out.map((x) => {
|
||||
return {
|
||||
label: x.nameDisplay || x.metadata.name,
|
||||
value: x.metadata.name
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -90,15 +117,17 @@ export default {
|
|||
class="row"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:mode="mode"
|
||||
:options="options"
|
||||
<ResourceLabeledSelect
|
||||
:resource-type="typeName"
|
||||
:in-store="inStore"
|
||||
:disabled="$fetchState.pending || disabled"
|
||||
:label="displayLabel"
|
||||
:placeholder="question.description"
|
||||
:required="question.required"
|
||||
:value="value"
|
||||
:tooltip="displayTooltip"
|
||||
:paginated-resource-settings="paginateResourceSetting"
|
||||
:all-resources-settings="allResourceSettings"
|
||||
@update:value="!$fetchState.pending && $emit('update:value', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
STATE, NAME, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, TYPE, AGE
|
||||
} from '@shell/config/table-headers';
|
||||
import { WORKLOAD_TYPES } from '@shell/config/types';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
|
||||
export default {
|
||||
components: { ResourceTable },
|
||||
props: {
|
||||
filter: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
// Enumerating instead of using Object.values() because it looks like we don't want to include replica sets for this.
|
||||
const types = [
|
||||
WORKLOAD_TYPES.DEPLOYMENT,
|
||||
WORKLOAD_TYPES.CRON_JOB,
|
||||
WORKLOAD_TYPES.DAEMON_SET,
|
||||
WORKLOAD_TYPES.JOB,
|
||||
WORKLOAD_TYPES.STATEFUL_SET
|
||||
];
|
||||
const allWorkloadsNested = await Promise.all(types.map((type) => this.$store.dispatch('cluster/findAll', { type })));
|
||||
const allWorkloads = allWorkloadsNested.flat();
|
||||
|
||||
this.relatedWorkloadRows = allWorkloads.filter(this.filter);
|
||||
},
|
||||
data() {
|
||||
const relatedWorkloadHeaders = [STATE, NAME, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, TYPE, AGE];
|
||||
|
||||
return {
|
||||
relatedWorkloadRows: [],
|
||||
relatedWorkloadHeaders,
|
||||
schema: this.$store.getters['cluster/schemaFor'](WORKLOAD_TYPES.DEPLOYMENT)
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ResourceTable
|
||||
:schema="schema"
|
||||
:rows="relatedWorkloadRows"
|
||||
:headers="relatedWorkloadHeaders"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -443,6 +443,9 @@ export default {
|
|||
<router-link
|
||||
v-if="location"
|
||||
:to="location"
|
||||
role="link"
|
||||
class="masthead-resource-list-link"
|
||||
:aria-label="parent.displayName"
|
||||
>
|
||||
{{ parent.displayName }}:
|
||||
</router-link>
|
||||
|
|
@ -584,10 +587,10 @@ export default {
|
|||
}
|
||||
|
||||
HEADER {
|
||||
margin: 0;
|
||||
margin: 0 0 0 -5px;
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,7 +601,7 @@ export default {
|
|||
|
||||
h1 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
@ -606,9 +609,13 @@ export default {
|
|||
.masthead-resource-title {
|
||||
padding: 0 8px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.masthead-resource-list-link {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ export default {
|
|||
:is="asLink ? 'a' : 'div'"
|
||||
v-for="(r, idx) in rows"
|
||||
:key="get(r, keyField)"
|
||||
:role="asLink ? 'link' : null"
|
||||
:aria-disabled="asLink && get(r, disabledField) === true ? true : null"
|
||||
:aria-label="get(r, nameField)"
|
||||
:tabindex="get(r, disabledField) === true ? -1 : 0"
|
||||
:href="asLink ? get(r, linkField) : null"
|
||||
:target="get(r, targetField)"
|
||||
:rel="rel"
|
||||
|
|
@ -111,9 +115,12 @@ export default {
|
|||
:data-testid="componentTestid + '-' + get(r, nameField)"
|
||||
:class="{
|
||||
'has-description': !!get(r, descriptionField),
|
||||
'has-side-label': !!get(r, sideLabelField), [colorFor(r, idx)]: true, disabled: get(r, disabledField) === true
|
||||
'has-side-label': !!get(r, sideLabelField),
|
||||
[colorFor(r, idx)]: true,
|
||||
disabled: get(r, disabledField) === true
|
||||
}"
|
||||
@click="select(r, idx)"
|
||||
@keyup.enter.space="select(r, idx)"
|
||||
>
|
||||
<div
|
||||
class="side-label"
|
||||
|
|
@ -212,6 +219,10 @@ export default {
|
|||
text-decoration: none !important;
|
||||
color: $color;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
box-shadow: 0 0 30px var(--shadow);
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@ import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|||
import { TYPE_MODES } from '@shell/store/type-map';
|
||||
import { NAME as NAVLINKS } from '@shell/config/product/navlinks';
|
||||
import Group from '@shell/components/nav/Group';
|
||||
import LocaleSelector from '@shell/components/LocaleSelector';
|
||||
|
||||
export default {
|
||||
name: 'SideNav',
|
||||
components: { Group },
|
||||
components: { Group, LocaleSelector },
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
|
|
@ -112,9 +113,7 @@ export default {
|
|||
computed: {
|
||||
...mapState(['managementReady', 'clusterReady']),
|
||||
...mapGetters(['isStandaloneHarvester', 'productId', 'clusterId', 'currentProduct', 'rootProduct', 'isSingleProduct', 'namespaceMode', 'isExplorer', 'isVirtualCluster']),
|
||||
...mapGetters({
|
||||
locale: 'i18n/selectedLocaleLabel', availableLocales: 'i18n/availableLocales', hasMultipleLocales: 'i18n/hasMultipleLocales'
|
||||
}),
|
||||
...mapGetters({ locale: 'i18n/selectedLocaleLabel', hasMultipleLocales: 'i18n/hasMultipleLocales' }),
|
||||
...mapGetters('type-map', ['activeProducts']),
|
||||
|
||||
favoriteTypes: mapPref(FAVORITE_TYPES),
|
||||
|
|
@ -360,10 +359,6 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
switchLocale(locale) {
|
||||
this.$store.dispatch('i18n/switchTo', locale);
|
||||
},
|
||||
|
||||
syncNav() {
|
||||
const refs = this.$refs.groups;
|
||||
|
||||
|
|
@ -427,6 +422,8 @@ export default {
|
|||
<router-link
|
||||
:to="supportLink"
|
||||
class="pull-right"
|
||||
role="link"
|
||||
:aria-label="t('nav.support', {hasSupport: true})"
|
||||
>
|
||||
{{ t('nav.support', {hasSupport: true}) }}
|
||||
</router-link>
|
||||
|
|
@ -439,36 +436,11 @@ export default {
|
|||
</span>
|
||||
|
||||
<!-- locale selector -->
|
||||
<span v-if="isSingleProduct && hasMultipleLocales && !isStandaloneHarvester">
|
||||
<v-dropdown
|
||||
popperClass="localeSelector"
|
||||
placement="top"
|
||||
:triggers="['click']"
|
||||
>
|
||||
<a
|
||||
data-testid="locale-selector"
|
||||
class="locale-chooser"
|
||||
>
|
||||
{{ locale }}
|
||||
</a>
|
||||
|
||||
<template #popper>
|
||||
<ul
|
||||
class="list-unstyled dropdown"
|
||||
style="margin: -1px;"
|
||||
>
|
||||
<li
|
||||
v-for="(label, name) in availableLocales"
|
||||
:key="name"
|
||||
class="hand"
|
||||
@click="switchLocale(name)"
|
||||
>
|
||||
{{ label }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
</span>
|
||||
<LocaleSelector
|
||||
v-if="isSingleProduct && hasMultipleLocales && !isStandaloneHarvester"
|
||||
mode="login"
|
||||
:show-icon="false"
|
||||
/>
|
||||
</div>
|
||||
<!-- SideNav footer alternative -->
|
||||
<div
|
||||
|
|
@ -478,6 +450,8 @@ export default {
|
|||
<router-link
|
||||
v-if="singleProductAbout"
|
||||
:to="singleProductAbout"
|
||||
role="link"
|
||||
:aria-label="t('nav.ariaLabel.productAboutPage')"
|
||||
>
|
||||
{{ displayVersion }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -258,7 +258,6 @@ export default {
|
|||
role="tablist"
|
||||
class="tabs"
|
||||
:class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
|
||||
tabindex="0"
|
||||
data-testid="tabbed-block"
|
||||
@keydown.right.prevent="selectNext(1)"
|
||||
@keydown.left.prevent="selectNext(-1)"
|
||||
|
|
@ -277,8 +276,12 @@ export default {
|
|||
:data-testid="`btn-${tab.name}`"
|
||||
:aria-controls="'#' + tab.name"
|
||||
:aria-selected="tab.active"
|
||||
:aria-label="tab.labelDisplay"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@click.prevent="select(tab.name, $event)"
|
||||
@keyup.enter="select(tab.name, $event)"
|
||||
@keyup.space="select(tab.name, $event)"
|
||||
>
|
||||
<span>{{ tab.labelDisplay }}</span>
|
||||
<span
|
||||
|
|
@ -403,6 +406,14 @@ export default {
|
|||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conditions-alert-icon {
|
||||
|
|
|
|||
|
|
@ -85,9 +85,11 @@ export default {
|
|||
class="name"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>{{ t('principal.name') }}: </td><td>{{ principal.name || principal.loginName }}</td></tr>
|
||||
<tr><td>{{ t('principal.loginName') }}: </td><td>{{ principal.loginName }}</td></tr>
|
||||
<tr><td>{{ t('principal.type') }}: </td><td>{{ principal.displayType }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<template v-else>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import Tag from '@shell/components/Tag.vue';
|
||||
import { STATE, NAME, AGE, FLEET_SUMMARY } from '@shell/config/table-headers';
|
||||
import { FLEET, MANAGEMENT } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
components: { ResourceTable },
|
||||
components: { ResourceTable, Tag },
|
||||
|
||||
props: {
|
||||
rows: {
|
||||
|
|
@ -75,6 +76,12 @@ export default {
|
|||
pluralLabel: this.$store.getters['type-map/labelFor'](schema, 99),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleCustomLabels(row) {
|
||||
row['displayCustomLabels'] = !row.displayCustomLabels;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -85,6 +92,7 @@ export default {
|
|||
:schema="schema"
|
||||
:headers="headers"
|
||||
:rows="rows"
|
||||
:sub-rows="true"
|
||||
:loading="loading"
|
||||
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
||||
key-field="_key"
|
||||
|
|
@ -123,5 +131,78 @@ export default {
|
|||
:class="{'text-error': !row.bundleInfo.total}"
|
||||
>{{ row.bundleInfo.total }}</span>
|
||||
</template>
|
||||
|
||||
<template #sub-row="{fullColspan, row, onRowMouseEnter, onRowMouseLeave}">
|
||||
<tr
|
||||
class="labels-row sub-row"
|
||||
@mouseenter="onRowMouseEnter"
|
||||
@mouseleave="onRowMouseLeave"
|
||||
>
|
||||
<template v-if="row.customLabels.length">
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td :colspan="fullColspan-2">
|
||||
<span
|
||||
v-if="row.customLabels.length"
|
||||
class="mt-5"
|
||||
> {{ t('fleet.cluster.labels') }}:
|
||||
<span
|
||||
v-for="(label, i) in row.customLabels"
|
||||
:key="i"
|
||||
class="mt-5 labels"
|
||||
>
|
||||
<Tag
|
||||
v-if="i < 7"
|
||||
class="mr-5 label"
|
||||
>
|
||||
{{ label }}
|
||||
</Tag>
|
||||
<Tag
|
||||
v-else-if="i > 6 && row.displayCustomLabels"
|
||||
class="mr-5 label"
|
||||
>
|
||||
{{ label }}
|
||||
</Tag>
|
||||
</span>
|
||||
<a
|
||||
v-if="row.customLabels.length > 7"
|
||||
href="#"
|
||||
@click.prevent="toggleCustomLabels(row)"
|
||||
>
|
||||
{{ t(`fleet.cluster.${row.displayCustomLabels? 'hideLabels' : 'showLabels'}`) }}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
<td
|
||||
v-else
|
||||
:colspan="fullColspan"
|
||||
>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.labels-row {
|
||||
td {
|
||||
padding-top:0;
|
||||
.tag {
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.labels {
|
||||
display: inline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,17 +6,15 @@ import FleetIntro from '@shell/components/fleet/FleetIntro';
|
|||
|
||||
import {
|
||||
AGE,
|
||||
STATE,
|
||||
NAME,
|
||||
FLEET_SUMMARY,
|
||||
FLEET_REPO,
|
||||
FLEET_REPO_TARGET,
|
||||
FLEET_REPO_CLUSTERS_READY,
|
||||
FLEET_REPO_CLUSTER_SUMMARY,
|
||||
FLEET_REPO_PER_CLUSTER_STATE
|
||||
|
||||
FLEET_REPO_CLUSTERS_READY,
|
||||
FLEET_REPO_PER_CLUSTER_STATE,
|
||||
FLEET_REPO_TARGET,
|
||||
FLEET_SUMMARY,
|
||||
NAME,
|
||||
STATE,
|
||||
} from '@shell/config/table-headers';
|
||||
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
||||
|
||||
// i18n-ignore repoDisplay
|
||||
export default {
|
||||
|
|
@ -77,31 +75,18 @@ export default {
|
|||
|
||||
headers() {
|
||||
// Cluster summary is only shown in the cluster view
|
||||
const fleetClusterSummary = {
|
||||
const summary = this.isClusterView ? [{
|
||||
...FLEET_REPO_CLUSTER_SUMMARY,
|
||||
formatterOpts: {
|
||||
// Fleet uses labels to identify clusters
|
||||
clusterLabel: this.clusterId
|
||||
},
|
||||
};
|
||||
formatterOpts: { clusterId: this.clusterId },
|
||||
}] : [FLEET_REPO_CLUSTERS_READY, FLEET_SUMMARY];
|
||||
|
||||
// if hasPerClusterState then use the repo state
|
||||
const fleetPerClusterState = {
|
||||
const state = this.isClusterView ? {
|
||||
...FLEET_REPO_PER_CLUSTER_STATE,
|
||||
value: (row) => {
|
||||
const statePerCluster = row.clusterResourceStatus?.find((c) => {
|
||||
return c.clusterLabel === this.clusterId;
|
||||
});
|
||||
value: (repo) => repo.clusterState(this.clusterId),
|
||||
} : STATE;
|
||||
|
||||
return statePerCluster ? statePerCluster?.status?.displayStatus : STATES_ENUM.ACTIVE;
|
||||
},
|
||||
};
|
||||
|
||||
const summary = this.isClusterView ? [fleetClusterSummary] : [FLEET_REPO_CLUSTERS_READY, FLEET_SUMMARY];
|
||||
|
||||
const state = this.isClusterView ? fleetPerClusterState : STATE;
|
||||
|
||||
const out = [
|
||||
return [
|
||||
state,
|
||||
NAME,
|
||||
FLEET_REPO,
|
||||
|
|
@ -109,8 +94,6 @@ export default {
|
|||
...summary,
|
||||
AGE
|
||||
];
|
||||
|
||||
return out;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,21 @@ export default {
|
|||
mounted() {
|
||||
// Ensures that if the default value is used, the model is updated with it
|
||||
this.$emit('update:value', this.inputValue);
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleKeyup(ev) {
|
||||
if (this.isDisabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.$refs.input.click(ev);
|
||||
}
|
||||
},
|
||||
|
||||
// according to https://www.w3.org/TR/html-aria/
|
||||
// input type="color" has no applicable role
|
||||
// and only aria-label and aria-disabled
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -82,6 +96,8 @@ export default {
|
|||
class="color-input"
|
||||
:class="{[mode]:mode, disabled: isDisabled}"
|
||||
:data-testid="componentTestid + '-color-input'"
|
||||
:tabindex="isDisabled ? -1 : 0"
|
||||
@keyup.enter.space.stop="handleKeyup($event)"
|
||||
>
|
||||
<label class="text-label"><t
|
||||
v-if="labelKey"
|
||||
|
|
@ -99,8 +115,11 @@ export default {
|
|||
>
|
||||
<input
|
||||
ref="input"
|
||||
:aria-disabled="isDisabled ? 'true' : 'false'"
|
||||
:aria-label="t('generic.colorPicker')"
|
||||
type="color"
|
||||
:disabled="isDisabled"
|
||||
tabindex="-1"
|
||||
:value="inputValue"
|
||||
@input="$emit('update:value', $event.target.value)"
|
||||
>
|
||||
|
|
@ -116,6 +135,10 @@ export default {
|
|||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
}
|
||||
|
||||
&.disabled, &.disabled .selected, &[disabled], &[disabled]:hover {
|
||||
color: var(--input-disabled-text);
|
||||
background-color: var(--input-disabled-bg);
|
||||
|
|
|
|||
|
|
@ -798,7 +798,7 @@ export default {
|
|||
v-if="addAllowed"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
data-testid="add_link_button"
|
||||
data-testid="add_row_item_button"
|
||||
:disabled="loading || disabled || (keyOptions && filteredKeyOptions.length === 0)"
|
||||
@click="add()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -154,11 +154,22 @@ export default {
|
|||
methods: {
|
||||
// resizeHandler = in mixin
|
||||
focusSearch() {
|
||||
const blurredAgo = Date.now() - this.blurred;
|
||||
// we need this override as in a "closeOnSelect" type of component
|
||||
// if we don't have this override, it would open again
|
||||
if (this.overridesMixinPreventDoubleTriggerKeysOpen) {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs['select'];
|
||||
|
||||
if ( el ) {
|
||||
el.focus();
|
||||
}
|
||||
|
||||
this.overridesMixinPreventDoubleTriggerKeysOpen = false;
|
||||
});
|
||||
|
||||
if (!this.focused && blurredAgo < 250) {
|
||||
return;
|
||||
}
|
||||
this.$refs['select-input'].open = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs['select-input']?.searchEl;
|
||||
|
|
@ -278,8 +289,9 @@ export default {
|
|||
'no-label': !hasLabel
|
||||
}
|
||||
]"
|
||||
:tabindex="isView || disabled ? -1 : 0"
|
||||
@click="focusSearch"
|
||||
@focus="focusSearch"
|
||||
@keyup.enter.space.down="focusSearch"
|
||||
>
|
||||
<div
|
||||
:class="{ 'labeled-container': true, raised, empty, [mode]: true }"
|
||||
|
|
@ -319,7 +331,7 @@ export default {
|
|||
:selectable="selectable"
|
||||
:modelValue="value != null && !loading ? value : ''"
|
||||
:dropdown-should-open="dropdownShouldOpen"
|
||||
|
||||
:tabindex="-1"
|
||||
@update:modelValue="$emit('selecting', $event); $emit('update:value', $event)"
|
||||
@search:blur="onBlur"
|
||||
@search:focus="onFocus"
|
||||
|
|
@ -384,7 +396,7 @@ export default {
|
|||
|
||||
<template #list-footer>
|
||||
<div
|
||||
v-if="canPaginate && totalResults"
|
||||
v-if="canPaginate && totalResults && pages > 1"
|
||||
class="pagination-slot"
|
||||
>
|
||||
<div class="load-more">
|
||||
|
|
|
|||
|
|
@ -54,11 +54,17 @@ export default {
|
|||
<button
|
||||
v-clean-tooltip="minusTooltip"
|
||||
:disabled="disabled || !canMinus"
|
||||
:aria-disabled="disabled || !canMinus"
|
||||
type="button"
|
||||
role="button"
|
||||
:aria-label="t('workload.plus')"
|
||||
class="btn btn-sm role-secondary"
|
||||
@click="$emit('minus')"
|
||||
>
|
||||
<i class="icon icon-sm icon-minus" />
|
||||
<i
|
||||
class="icon icon-sm icon-minus"
|
||||
:alt="t('workload.plus')"
|
||||
/>
|
||||
</button>
|
||||
<div class="value">
|
||||
{{ value }}
|
||||
|
|
@ -66,11 +72,17 @@ export default {
|
|||
<button
|
||||
v-clean-tooltip="plusTooltip"
|
||||
:disabled="disabled || !canPlus"
|
||||
:aria-disabled="disabled || !canPlus"
|
||||
type="button"
|
||||
role="button"
|
||||
:aria-label="t('workload.minus')"
|
||||
class="btn btn-sm role-secondary"
|
||||
@click="$emit('plus')"
|
||||
>
|
||||
<i class="icon icon-sm icon-plus" />
|
||||
<i
|
||||
class="icon icon-sm icon-plus"
|
||||
:alt="t('workload.minus')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,53 +3,8 @@ import { PropType, defineComponent } from 'vue';
|
|||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
import { labelSelectPaginationFunction, LabelSelectPaginationFunctionOptions } from '@shell/components/form/labeled-select-utils/labeled-select.utils';
|
||||
import { LabelSelectPaginateFn, LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '@shell/types/components/labeledSelect';
|
||||
|
||||
type PaginateTypeOverridesFn = (opts: LabelSelectPaginationFunctionOptions) => LabelSelectPaginationFunctionOptions;
|
||||
|
||||
interface SharedSettings {
|
||||
/**
|
||||
* Provide specific LabelSelect options for this mode (paginated / not paginated)
|
||||
*/
|
||||
labelSelectOptions?: { [key: string]: any },
|
||||
/**
|
||||
* Map the resources shown in LabelSelect
|
||||
*/
|
||||
mapResult?: (resources: any[]) => any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings to use when the LabelSelect is paginating
|
||||
*/
|
||||
export interface ResourceLabeledSelectPaginateSettings extends SharedSettings {
|
||||
/**
|
||||
* Override the convience function which fetches a page of results
|
||||
*/
|
||||
overrideRequest?: LabelSelectPaginateFn,
|
||||
/**
|
||||
* Override the default settings used in the convenience function to fetch a page of results
|
||||
*/
|
||||
requestSettings?: PaginateTypeOverridesFn,
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings to use when the LabelSelect is fetching all resources (not paginating)
|
||||
*/
|
||||
export type ResourceLabeledSelectSettings = SharedSettings
|
||||
|
||||
/**
|
||||
* Force a specific mode
|
||||
*/
|
||||
export enum RESOURCE_LABEL_SELECT_MODE {
|
||||
/**
|
||||
* Fetch all resources
|
||||
*/
|
||||
ALL_RESOURCES = 'ALL', // eslint-disable-line no-unused-vars
|
||||
/**
|
||||
* Determine if all resources are fetched given system settings
|
||||
*/
|
||||
DYNAMIC = 'DYNAMIC', // eslint-disable-line no-unused-vars
|
||||
}
|
||||
import { LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '@shell/types/components/labeledSelect';
|
||||
import { RESOURCE_LABEL_SELECT_MODE, ResourceLabeledSelectPaginateSettings, ResourceLabeledSelectSettings } from '@shell/types/components/resourceLabeledSelect';
|
||||
|
||||
/**
|
||||
* Convenience wrapper around the LabelSelect component to support pagination
|
||||
|
|
@ -66,6 +21,8 @@ export default defineComponent({
|
|||
|
||||
components: { LabeledSelect },
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
props: {
|
||||
/**
|
||||
* Resource to show
|
||||
|
|
@ -89,7 +46,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
/**
|
||||
* Specific settings to use when we're showing all results
|
||||
* Specific settings to use when we're showing all results in the drop down
|
||||
*/
|
||||
allResourcesSettings: {
|
||||
type: Object as PropType<ResourceLabeledSelectSettings>,
|
||||
|
|
@ -97,7 +54,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
/**
|
||||
* Specific settings to use when we're showing paginated results
|
||||
* Specific settings to use when we're showing paginated results in the drop down
|
||||
*/
|
||||
paginatedResourceSettings: {
|
||||
type: Object as PropType<ResourceLabeledSelectPaginateSettings>,
|
||||
|
|
@ -120,6 +77,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (!this.paginate) {
|
||||
// The resource won't be paginated and component expects everything up front
|
||||
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: this.resourceType });
|
||||
}
|
||||
},
|
||||
|
|
@ -148,13 +106,14 @@ export default defineComponent({
|
|||
|
||||
const all = this.$store.getters[`${ this.inStore }/all`](this.resourceType);
|
||||
|
||||
return this.allResourcesSettings?.mapResult ? this.allResourcesSettings.mapResult(all) : all;
|
||||
return this.allResourcesSettings?.updateResources ? this.allResourcesSettings.updateResources(all) : all;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Typeof LabelSelectPaginateFn
|
||||
* Make the request to fetch the resource given the state of the label select (filter, page, page size, etc see LabelSelectPaginateFn)
|
||||
* opts: Typeof LabelSelectPaginateFn
|
||||
*/
|
||||
async paginateType(opts: LabelSelectPaginateFnOptions): Promise<LabelSelectPaginateFnResponse> {
|
||||
if (this.paginatedResourceSettings?.overrideRequest) {
|
||||
|
|
@ -175,9 +134,9 @@ export default defineComponent({
|
|||
const options = this.paginatedResourceSettings?.requestSettings ? this.paginatedResourceSettings.requestSettings(defaultOptions) : defaultOptions;
|
||||
const res = await labelSelectPaginationFunction(options);
|
||||
|
||||
return this.paginatedResourceSettings?.mapResult ? {
|
||||
return this.paginatedResourceSettings?.updateResources ? {
|
||||
...res,
|
||||
page: this.paginatedResourceSettings.mapResult(res.page)
|
||||
page: this.paginatedResourceSettings.updateResources(res.page)
|
||||
} : res;
|
||||
},
|
||||
},
|
||||
|
|
@ -190,5 +149,6 @@ export default defineComponent({
|
|||
:loading="$fetchState.pending"
|
||||
:options="allOfType"
|
||||
:paginate="paginateType"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default {
|
|||
},
|
||||
|
||||
async fetch() {
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
const hash = await allHash({ allResources: this.$store.dispatch('cluster/findAll', { type: this.type }) });
|
||||
|
||||
this.allResources = hash.allResources;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import Tab from '@shell/components/Tabbed/Tab';
|
|||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import Conditions from '@shell/components/form/Conditions';
|
||||
import { EVENT } from '@shell/config/types';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import RelatedResources from '@shell/components/RelatedResources';
|
||||
import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
import { MESSAGE, REASON } from '@shell/config/table-headers';
|
||||
import { STEVE_EVENT_LAST_SEEN, STEVE_EVENT_TYPE, STEVE_NAME_COL } from '@shell/config/pagination-table-headers';
|
||||
import { headerFromSchemaColString } from '@shell/store/type-map.utils';
|
||||
|
||||
export default {
|
||||
|
||||
|
|
@ -21,7 +25,7 @@ export default {
|
|||
Tabbed,
|
||||
Tab,
|
||||
Conditions,
|
||||
SortableTable,
|
||||
PaginatedResourceTable,
|
||||
RelatedResources,
|
||||
},
|
||||
|
||||
|
|
@ -69,14 +73,25 @@ export default {
|
|||
|
||||
data() {
|
||||
const inStore = this.$store.getters['currentStore'](EVENT);
|
||||
const eventSchema = this.$store.getters[`${ inStore }/schemaFor`](EVENT); // @TODO be smarter about which resources actually ever have events
|
||||
|
||||
return {
|
||||
hasEvents: this.$store.getters[`${ inStore }/schemaFor`](EVENT), // @TODO be smarter about which resources actually ever have events
|
||||
allEvents: [],
|
||||
eventSchema,
|
||||
EVENT,
|
||||
selectedTab: this.defaultTab,
|
||||
didLoadEvents: false,
|
||||
inStore,
|
||||
showConditions: false,
|
||||
paginationHeaders: [
|
||||
STEVE_EVENT_LAST_SEEN,
|
||||
STEVE_EVENT_TYPE,
|
||||
REASON,
|
||||
headerFromSchemaColString('Subobject', eventSchema, this.$store.getters, true),
|
||||
headerFromSchemaColString('Source', eventSchema, this.$store.getters, true),
|
||||
MESSAGE,
|
||||
headerFromSchemaColString('First Seen', eventSchema, this.$store.getters, true),
|
||||
headerFromSchemaColString('Count', eventSchema, this.$store.getters, true),
|
||||
STEVE_NAME_COL,
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -92,7 +107,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
showEvents() {
|
||||
return this.isView && this.needEvents && this.hasEvents;
|
||||
return this.isView && this.needEvents && this.eventSchema;
|
||||
},
|
||||
showRelated() {
|
||||
return this.isView && this.needRelated;
|
||||
|
|
@ -128,18 +143,6 @@ export default {
|
|||
},
|
||||
];
|
||||
},
|
||||
events() {
|
||||
return this.allEvents.filter((event) => {
|
||||
return event.involvedObject?.uid === this.value?.metadata?.uid;
|
||||
}).map((event) => {
|
||||
return {
|
||||
reason: (`${ event.reason || this.t('generic.unknown') }${ event.count > 1 ? ` (${ event.count })` : '' }`).trim(),
|
||||
message: event.message || this.t('generic.unknown'),
|
||||
date: event.lastTimestamp || event.firstTimestamp || event.metadata.creationTimestamp,
|
||||
eventType: event.eventType
|
||||
};
|
||||
});
|
||||
},
|
||||
conditionsHaveIssues() {
|
||||
if (this.showConditions) {
|
||||
return this.value.status?.conditions?.filter((cond) => !isConditionReadyAndWaiting(cond)).some((cond) => cond.error);
|
||||
|
|
@ -153,15 +156,6 @@ export default {
|
|||
// Ensures we only fetch events and show the table when the events tab has been activated
|
||||
tabChange(neu) {
|
||||
this.selectedTab = neu?.selectedName;
|
||||
|
||||
if (!this.didLoadEvents && this.selectedTab === 'events') {
|
||||
const inStore = this.$store.getters['currentStore'](EVENT);
|
||||
|
||||
this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }).then((events) => {
|
||||
this.allEvents = events;
|
||||
this.didLoadEvents = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -180,6 +174,54 @@ export default {
|
|||
this.showConditions = this.$store.getters[`${ this.inStore }/pathExistsInSchema`](this.value.type, 'status.conditions');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out hidden repos from list of all repos
|
||||
*/
|
||||
filterEventsLocal(rows) {
|
||||
return rows.filter((event) => event.involvedObject?.uid === this.value?.metadata?.uid);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out hidden repos via api
|
||||
*
|
||||
* pagination: PaginationArgs
|
||||
* returns: PaginationArgs
|
||||
*/
|
||||
filterEventsApi(pagination) {
|
||||
if (!pagination.filters) {
|
||||
pagination.filters = [];
|
||||
}
|
||||
|
||||
const field = `involvedObject.uid`; // Pending API Support - https://github.com/rancher/rancher/issues/48603
|
||||
|
||||
// of type PaginationParamFilter
|
||||
let existing = null;
|
||||
|
||||
for (let i = 0; i < pagination.filters.length; i++) {
|
||||
const filter = pagination.filters[i];
|
||||
|
||||
if (!!filter.fields.find((f) => f.field === field)) {
|
||||
existing = filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const required = PaginationParamFilter.createSingleField({
|
||||
field,
|
||||
exact: true,
|
||||
value: this.value.metadata.uid,
|
||||
equals: true
|
||||
});
|
||||
|
||||
if (!!existing) {
|
||||
Object.assign(existing, required);
|
||||
} else {
|
||||
pagination.filters.push(required);
|
||||
}
|
||||
|
||||
return pagination;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -208,15 +250,16 @@ export default {
|
|||
name="events"
|
||||
:weight="-2"
|
||||
>
|
||||
<SortableTable
|
||||
<!-- namespaced: false given we don't want the default handling of namespaced resource (apply header filter) -->
|
||||
<PaginatedResourceTable
|
||||
v-if="selectedTab === 'events'"
|
||||
:rows="events"
|
||||
:schema="eventSchema"
|
||||
:local-filter="filterEventsLocal"
|
||||
:api-filter="filterEventsApi"
|
||||
:use-query-params-for-simple-filtering="false"
|
||||
:headers="eventHeaders"
|
||||
key-field="id"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
default-sort-by="date"
|
||||
:paginationHeaders="paginationHeaders"
|
||||
:namespaced="false"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default {
|
|||
secrets: null,
|
||||
SECRET,
|
||||
allSecretsSettings: {
|
||||
mapResult: (secrets) => {
|
||||
updateResources: (secrets) => {
|
||||
const allSecretsInNamespace = secrets.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
||||
const mappedSecrets = this.mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ export default {
|
|||
},
|
||||
paginateSecretsSetting: {
|
||||
requestSettings: this.paginatePageOptions,
|
||||
mapResult: (secrets) => {
|
||||
updateResources: (secrets) => {
|
||||
const mappedSecrets = this.mapSecrets(secrets);
|
||||
|
||||
this.secrets = secrets; // We need the key from the selected secret. When paginating we won't touch the store, so just pass back here
|
||||
|
|
|
|||
|
|
@ -114,11 +114,24 @@ export default {
|
|||
calculatePosition(dropdownList, component, width, this.placement);
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.focusSearch();
|
||||
},
|
||||
|
||||
focusSearch() {
|
||||
// we need this override as in a "closeOnSelect" type of component
|
||||
// if we don't have this override, it would open again
|
||||
if (this.overridesMixinPreventDoubleTriggerKeysOpen) {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs['select'];
|
||||
|
||||
if ( el ) {
|
||||
el.focus();
|
||||
}
|
||||
|
||||
this.overridesMixinPreventDoubleTriggerKeysOpen = false;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.$refs['select-input'].open = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs['select-input']?.searchEl;
|
||||
|
||||
|
|
@ -176,6 +189,11 @@ export default {
|
|||
},
|
||||
report(e) {
|
||||
alert(e);
|
||||
},
|
||||
handleDropdownOpen(args) {
|
||||
// function that prevents the "opening dropdown on focus"
|
||||
// default behaviour of v-select
|
||||
return args.noDrop || args.disabled ? false : args.open;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -227,7 +245,7 @@ export default {
|
|||
ref="select"
|
||||
class="unlabeled-select"
|
||||
:class="{
|
||||
disabled: disabled && !isView,
|
||||
disabled: disabled || isView,
|
||||
focused,
|
||||
[mode]: true,
|
||||
[status]: status,
|
||||
|
|
@ -236,7 +254,9 @@ export default {
|
|||
'compact-input': compact,
|
||||
[$attrs.class]: $attrs.class
|
||||
}"
|
||||
@focus="focusSearch"
|
||||
:tabindex="disabled || isView ? -1 : 0"
|
||||
@click="focusSearch"
|
||||
@keyup.enter.space.down="focusSearch"
|
||||
>
|
||||
<v-select
|
||||
ref="select-input"
|
||||
|
|
@ -258,6 +278,8 @@ export default {
|
|||
:searchable="isSearchable"
|
||||
:selectable="selectable"
|
||||
:modelValue="value != null ? value : ''"
|
||||
:dropdownShouldOpen="handleDropdownOpen"
|
||||
:tabindex="-1"
|
||||
|
||||
@update:modelValue="$emit('update:value', $event)"
|
||||
@search:blur="onBlur"
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ describe('component: KeyValue', () => {
|
|||
expect(secondKeyInput.exists()).toBe(false);
|
||||
expect(secondValueInput.exists()).toBe(false);
|
||||
|
||||
const addButton = wrapper.find('[data-testid="add_link_button"]');
|
||||
const addButton = wrapper.find('[data-testid="add_row_item_button"]');
|
||||
|
||||
addButton.trigger('click');
|
||||
await nextTick();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default {
|
|||
required: true
|
||||
},
|
||||
|
||||
clusterLabel: {
|
||||
clusterId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
|
|
@ -22,6 +22,6 @@ export default {
|
|||
<template>
|
||||
<FleetSummaryGraph
|
||||
:row="row"
|
||||
:clusterLabel="clusterLabel"
|
||||
:clusterId="clusterId"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default {
|
|||
required: true
|
||||
},
|
||||
|
||||
clusterLabel: {
|
||||
clusterId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
|
|
@ -23,10 +23,8 @@ export default {
|
|||
|
||||
computed: {
|
||||
summary() {
|
||||
if (this.clusterLabel) {
|
||||
return this.row.clusterResourceStatus.find((x) => {
|
||||
return x.clusterLabel === this.clusterLabel;
|
||||
})?.status.resourceCounts || {};
|
||||
if (this.clusterId) {
|
||||
return this.row.statusResourceCountsForCluster(this.clusterId);
|
||||
}
|
||||
|
||||
return this.row.status?.resourceCounts || {};
|
||||
|
|
@ -37,7 +35,8 @@ export default {
|
|||
},
|
||||
|
||||
stateParts() {
|
||||
const keys = Object.keys(this.summary).filter((x) => !x.startsWith('desired'));
|
||||
const summary = this.summary;
|
||||
const keys = Object.keys(summary).filter((x) => !x.startsWith('desired'));
|
||||
|
||||
const out = keys.map((key) => {
|
||||
const textColor = colorForState(key);
|
||||
|
|
@ -46,7 +45,7 @@ export default {
|
|||
label: ucFirst(key),
|
||||
color: textColor.replace(/text-/, 'bg-'),
|
||||
textColor,
|
||||
value: this.summary[key],
|
||||
value: summary[key],
|
||||
sort: stateSort(textColor, key),
|
||||
};
|
||||
}).filter((x) => x.value > 0);
|
||||
|
|
|
|||
|
|
@ -182,14 +182,21 @@ export default {
|
|||
<div
|
||||
id="trigger"
|
||||
class="hs-popover__trigger"
|
||||
aria-role="button"
|
||||
tabindex="0"
|
||||
:class="{expanded}"
|
||||
:aria-roledescription="t('workload.scaleWorkloads')"
|
||||
:aria-label="t('workload.healthScaleToggle')"
|
||||
:aria-expanded="expanded"
|
||||
@click="expanded = !expanded"
|
||||
@keyup.enter.space="expanded = !expanded"
|
||||
>
|
||||
<ProgressBarMulti
|
||||
v-if="parts"
|
||||
class="health"
|
||||
:values="parts"
|
||||
:show-zeros="true"
|
||||
:aria-describedby="t('workload.healthWorkloads')"
|
||||
/>
|
||||
<i :class="{icon: true, 'icon-chevron-up': expanded, 'icon-chevron-down': !expanded}" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -211,26 +211,40 @@ export default {
|
|||
v-if="showHeader"
|
||||
class="header"
|
||||
:class="{'active': isOverview, 'noHover': !canCollapse}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="group.labelDisplay || group.label || ''"
|
||||
@click="groupSelected()"
|
||||
@keyup.enter="groupSelected()"
|
||||
@keyup.space="groupSelected()"
|
||||
>
|
||||
<slot name="header">
|
||||
<router-link
|
||||
v-if="hasOverview"
|
||||
:to="group.children[0].route"
|
||||
:exact="group.children[0].exact"
|
||||
:tabindex="-1"
|
||||
>
|
||||
<h6 v-clean-html="group.labelDisplay || group.label" />
|
||||
<h6>
|
||||
<span v-clean-html="group.labelDisplay || group.label" />
|
||||
</h6>
|
||||
</router-link>
|
||||
<h6
|
||||
v-else
|
||||
v-clean-html="group.labelDisplay || group.label"
|
||||
/>
|
||||
>
|
||||
<span v-clean-html="group.labelDisplay || group.label" />
|
||||
</h6>
|
||||
</slot>
|
||||
<i
|
||||
v-if="!onlyHasOverview && canCollapse"
|
||||
class="icon toggle"
|
||||
class="icon toggle toggle-accordion"
|
||||
:class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="t('nav.ariaLabel.collapseExpand')"
|
||||
@click="peek($event, true)"
|
||||
@keyup.enter="peek($event, true)"
|
||||
@keyup.space="peek($event, true)"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
|
|
@ -288,6 +302,7 @@ export default {
|
|||
cursor: pointer;
|
||||
color: var(--body-text);
|
||||
height: 33px;
|
||||
outline: none;
|
||||
|
||||
H6 {
|
||||
color: var(--body-text);
|
||||
|
|
@ -315,6 +330,17 @@ export default {
|
|||
|
||||
.accordion {
|
||||
.header {
|
||||
&:focus-visible {
|
||||
h6 span {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
.toggle-accordion:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: -6px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary-hover-text);
|
||||
background-color: var(--primary-hover-bg);
|
||||
|
|
|
|||
|
|
@ -118,7 +118,10 @@ export default {
|
|||
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
||||
</TabTitle>
|
||||
<a
|
||||
role="link"
|
||||
:aria-label="type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label)"
|
||||
:href="href"
|
||||
class="type-link"
|
||||
@click="selectType(); navigate($event);"
|
||||
@mouseenter="setNear(true)"
|
||||
@mouseleave="setNear(false)"
|
||||
|
|
@ -161,9 +164,11 @@ export default {
|
|||
data-testid="link-type"
|
||||
>
|
||||
<a
|
||||
role="link"
|
||||
:href="type.link"
|
||||
:target="type.target"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:aria-label="type.label"
|
||||
>
|
||||
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
||||
</a>
|
||||
|
|
@ -178,6 +183,16 @@ export default {
|
|||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.type-link:focus-visible span.label {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.nav-link a:focus-visible .label {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.child {
|
||||
margin: 0 var(--outline) 0 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,8 @@ export const FLEET = {
|
|||
CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name',
|
||||
CLUSTER_NAME: 'management.cattle.io/cluster-name',
|
||||
BUNDLE_ID: 'fleet.cattle.io/bundle-id',
|
||||
BUNDLE_NAME: 'fleet.cattle.io/bundle-name',
|
||||
BUNDLE_NAMESPACE: 'fleet.cattle.io/bundle-namespace',
|
||||
MANAGED: 'fleet.cattle.io/managed',
|
||||
CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
|
||||
CLUSTER: 'fleet.cattle.io/cluster'
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue