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'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ inputs.registry_target }}
|
registry: ${{ inputs.registry_target }}
|
||||||
username: ${{ inputs.registry_user }}
|
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> {
|
checkNotExists(options?: GetOptions): Cypress.Chainable<boolean> {
|
||||||
return this.self(options).should('not.exist');
|
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
|
* Return the input HTML element from given container
|
||||||
* @returns HTML Element
|
* @returns HTML Element
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ export default class TabbedPo extends ComponentPo {
|
||||||
return this.self().get('[data-testid="tabbed-block"] > li');
|
return this.self().get('[data-testid="tabbed-block"] > li');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertTabIsActive(selector: string) {
|
||||||
|
return this.self().find(`${ selector }`).should('have.class', 'active');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tab labels
|
* Get tab labels
|
||||||
* @param tabLabelsSelector
|
* @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 AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
|
||||||
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
|
||||||
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
|
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
|
||||||
import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po';
|
import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po';
|
||||||
import BannersPo from '@/cypress/e2e/po/components/banners.po';
|
import BannersPo from '@/cypress/e2e/po/components/banners.po';
|
||||||
import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po';
|
import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po';
|
||||||
export default class NetworkPolicyPo extends CreateEditViewPo {
|
import ArrayListPo from '@/cypress/e2e/po/components/array-list.po';
|
||||||
constructor(selector = '.dashboard-root') {
|
|
||||||
super(selector);
|
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() {
|
nameInput() {
|
||||||
|
|
@ -30,6 +42,14 @@ export default class NetworkPolicyPo extends CreateEditViewPo {
|
||||||
return cy.get('[data-testid="array-list-button"]').contains('Add allowed traffic source');
|
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) {
|
policyRuleTargetSelect(index: number) {
|
||||||
return new LabeledSelectPo(`[data-testid="policy-rule-target-${ index }"] [data-testid="policy-rule-target-type-labeled-select"]`, this.self());
|
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';
|
import ResourceYamlPo from '@/cypress/e2e/po/components/resource-yaml.po';
|
||||||
|
|
||||||
export default class ResourceDetailPo extends ComponentPo {
|
export default class ResourceDetailPo extends ComponentPo {
|
||||||
|
/**
|
||||||
|
* components for handling CRUD operations for resources, including cancel/save buttons
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
cruResource() {
|
cruResource() {
|
||||||
return new CruResourcePo(this.self());
|
return new CruResourcePo(this.self());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* components for managing the resource creation and edit forms
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
createEditView() {
|
createEditView() {
|
||||||
return new CreateEditViewPo(this.self());
|
return new CreateEditViewPo(this.self());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* components for YAML editor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
resourceYaml() {
|
resourceYaml() {
|
||||||
return new ResourceYamlPo(this.self());
|
return new ResourceYamlPo(this.self());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,76 @@
|
||||||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
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 {
|
export default class ServicesCreateEditPo extends PagePo {
|
||||||
private static createPath(clusterId: string, id?: string ) {
|
private static createPath(clusterId: string, namespace?: string, id?: string ) {
|
||||||
const root = `/c/${ clusterId }/explorer/storage.k8s.io.storageclass/create`;
|
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> {
|
static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||||
throw new Error('invalid');
|
throw new Error('invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clusterId = '_', id?: string) {
|
constructor(clusterId = 'local', namespace?: string, id?: string) {
|
||||||
super(WorkloadsCreateEditPo.createPath(clusterId, id));
|
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() {
|
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);
|
return this.resourceTable().sortableTable().rowActionMenuOpen(repoName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeActionMenu() {
|
||||||
|
cy.get('body').click(0, 0); // Click outside of the action menu
|
||||||
|
}
|
||||||
|
|
||||||
openBulkActionDropdown() {
|
openBulkActionDropdown() {
|
||||||
return this.resourceTable().sortableTable().bulkActionDropDownOpen();
|
return this.resourceTable().sortableTable().bulkActionDropDownOpen();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,8 @@ export default class FleetClusterList extends BaseResourceList {
|
||||||
details(name: string, index: number) {
|
details(name: string, index: number) {
|
||||||
return this.resourceTable().sortableTable().rowWithName(name).column(index);
|
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 BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
|
||||||
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.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 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 {
|
export class NetworkPolicyPagePo extends PagePo {
|
||||||
private static createPath(clusterId: string) {
|
private static createPath(clusterId: string) {
|
||||||
|
|
@ -21,7 +22,7 @@ export class NetworkPolicyPagePo extends PagePo {
|
||||||
sideNav.navToSideMenuEntryByLabel('Network Policies');
|
sideNav.navToSideMenuEntryByLabel('Network Policies');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clusterId = 'local') {
|
constructor(private clusterId = 'local') {
|
||||||
super(NetworkPolicyPagePo.createPath(clusterId));
|
super(NetworkPolicyPagePo.createPath(clusterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,4 +41,8 @@ export class NetworkPolicyPagePo extends PagePo {
|
||||||
searchForNetworkPolicy(name: string) {
|
searchForNetworkPolicy(name: string) {
|
||||||
return this.list().resourceTable().sortableTable().filter(name);
|
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');
|
sideNav.navToSideMenuEntryByLabel('Service');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clusterId = 'local') {
|
constructor(private clusterId = 'local') {
|
||||||
super(ServicesPagePo.createPath(clusterId));
|
super(ServicesPagePo.createPath(clusterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ export class ServicesPagePo extends PagePo {
|
||||||
return this.list().masthead().create();
|
return this.list().masthead().create();
|
||||||
}
|
}
|
||||||
|
|
||||||
createServicesForm(id? : string): ServicesCreateEditPo {
|
createServicesForm(namespace?: string, id?: string): ServicesCreateEditPo {
|
||||||
return new ServicesCreateEditPo(id);
|
return new ServicesCreateEditPo(this.clusterId, namespace, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default class ExtensionsPagePo extends PagePo {
|
||||||
return this.title().should('contain', 'Extensions');
|
return this.title().should('contain', 'Extensions');
|
||||||
}
|
}
|
||||||
|
|
||||||
loading(options: any) {
|
loading() {
|
||||||
return this.self().get('.data-loading');
|
return this.self().get('.data-loading');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class HomeLinksPagePo extends RootClusterPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
addLinkButton() {
|
addLinkButton() {
|
||||||
return cy.getId('add_link_button');
|
return cy.getId('add_row_item_button');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeLinkButton() {
|
removeLinkButton() {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ export class PerformancePagePo extends RootClusterPage {
|
||||||
return CheckboxInputPo.byLabel(this.self(), 'Enable Garbage Collection');
|
return CheckboxInputPo.byLabel(this.self(), 'Enable Garbage Collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
garbageCollectionResourceCount() {
|
||||||
|
return LabeledInputPo.byLabel(this.self(), 'Resource Count');
|
||||||
|
}
|
||||||
|
|
||||||
namespaceFilteringCheckbox(): CheckboxInputPo {
|
namespaceFilteringCheckbox(): CheckboxInputPo {
|
||||||
return CheckboxInputPo.byLabel(this.self(), 'Enable Required Namespace / Project Filtering');
|
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);
|
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 { 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';
|
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||||
|
|
||||||
const networkPolicyPage = new NetworkPolicyPagePo('local');
|
const networkPolicyPage = new NetworkPolicyPagePo('local');
|
||||||
const networkPolicyName = 'custom-network-policy';
|
const customNetworkPolicyName = 'custom-network-policy';
|
||||||
const networkPolicyDescription = 'Custom Network Policy Description';
|
const networkPolicyDescription = 'Custom Network Policy Description';
|
||||||
|
const namespace = 'default';
|
||||||
|
let networkPolicyName = '';
|
||||||
|
let removeNetworkPolicy = false;
|
||||||
|
const networkPolicyToDelete = [];
|
||||||
|
|
||||||
describe('NetworkPolicies', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
describe('NetworkPolicies', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
|
cy.createE2EResourceName('networkpolicy').then((name) => {
|
||||||
|
networkPolicyName = name;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a network policy and displays it in the list', () => {
|
it('creates a network policy and displays it in the list', () => {
|
||||||
// Visit the main menu and select the 'local' cluster
|
// Visit the main menu and select the 'local' cluster
|
||||||
// Navigate to Policy => Network Policies
|
// Navigate to Policy => Network Policies
|
||||||
NetworkPolicyPagePo.navTo();
|
NetworkPolicyPagePo.navTo();
|
||||||
|
networkPolicyPage.waitForPage();
|
||||||
// Go to Create Page
|
// Go to Create Page
|
||||||
networkPolicyPage.clickCreate();
|
networkPolicyPage.clickCreate();
|
||||||
const networkPolicyPo = new NetworkPolicyPo();
|
|
||||||
|
|
||||||
// Enter name & description
|
// Enter name & description
|
||||||
networkPolicyPo.nameInput().set(networkPolicyName);
|
networkPolicyPage.createEditNetworkPolicyForm().nameInput().set(customNetworkPolicyName);
|
||||||
networkPolicyPo.descriptionInput().set(networkPolicyDescription);
|
networkPolicyPage.createEditNetworkPolicyForm().descriptionInput().set(networkPolicyDescription);
|
||||||
// Enable ingress checkbox
|
// Enable ingress checkbox
|
||||||
networkPolicyPo.enableIngressCheckbox().set();
|
networkPolicyPage.createEditNetworkPolicyForm().enableIngressCheckbox().set();
|
||||||
// Add a new rule without a key to match all the namespaces
|
// Add a new rule without a key to match all the namespaces
|
||||||
networkPolicyPo.newNetworkPolicyRuleAddBtn().click();
|
networkPolicyPage.createEditNetworkPolicyForm().newNetworkPolicyRuleAddBtn().click();
|
||||||
networkPolicyPo.addAllowedTrafficSourceButton().click();
|
networkPolicyPage.createEditNetworkPolicyForm().addAllowedTrafficSourceButton().click();
|
||||||
networkPolicyPo.policyRuleTargetSelect(0).toggle();
|
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(0).toggle();
|
||||||
networkPolicyPo.policyRuleTargetSelect(0).clickOptionWithLabel('Namespace Selector');
|
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(0).clickOptionWithLabel('Namespace Selector');
|
||||||
|
|
||||||
cy.getRancherResource('v1', 'namespaces').then((resp: Cypress.Response<any>) => {
|
cy.getRancherResource('v1', 'namespaces').then((resp: Cypress.Response<any>) => {
|
||||||
cy.wrap(resp.body.count).as('namespaceCount');
|
cy.wrap(resp.body.count).as('namespaceCount');
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('@namespaceCount').then((count) => {
|
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
|
// Add a second rule a key to match none of the namespaces
|
||||||
networkPolicyPo.addAllowedTrafficSourceButton().click();
|
networkPolicyPage.createEditNetworkPolicyForm().addAllowedTrafficSourceButton().click();
|
||||||
networkPolicyPo.policyRuleTargetSelect(1).toggle();
|
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(1).toggle();
|
||||||
networkPolicyPo.policyRuleTargetSelect(1).clickOptionWithLabel('Namespace Selector');
|
networkPolicyPage.createEditNetworkPolicyForm().policyRuleTargetSelect(1).clickOptionWithLabel('Namespace Selector');
|
||||||
networkPolicyPo.policyRuleKeyInput(1).focus().type('something-with-no-matching-namespaces');
|
networkPolicyPage.createEditNetworkPolicyForm().policyRuleKeyInput(1).focus().type('something-with-no-matching-namespaces');
|
||||||
networkPolicyPo.matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||||
// Click on Create
|
// Click on Create
|
||||||
networkPolicyPo.saveCreateForm().click();
|
networkPolicyPage.createEditNetworkPolicyForm().saveCreateForm().click();
|
||||||
// Check if the NetworkPolicy is created successfully
|
// Check if the NetworkPolicy is created successfully
|
||||||
networkPolicyPage.waitForPage();
|
networkPolicyPage.waitForPage();
|
||||||
networkPolicyPage.searchForNetworkPolicy(networkPolicyName);
|
networkPolicyPage.searchForNetworkPolicy(customNetworkPolicyName);
|
||||||
networkPolicyPage.waitForPage(`q=${ networkPolicyName }`);
|
networkPolicyPage.waitForPage(`q=${ customNetworkPolicyName }`);
|
||||||
networkPolicyPage.listElementWithName(networkPolicyName).should('exist').and('be.visible');
|
networkPolicyPage.listElementWithName(customNetworkPolicyName).should('exist').and('be.visible');
|
||||||
// Navigate back to the edit page and check if the matching message is still correct
|
// Navigate back to the edit page and check if the matching message is still correct
|
||||||
networkPolicyPage.list().actionMenu(networkPolicyName).getMenuItem('Edit Config').click();
|
networkPolicyPage.list().actionMenu(customNetworkPolicyName).getMenuItem('Edit Config').click();
|
||||||
networkPolicyPo.matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(0).should('contain.text', `Matches ${ count } of ${ count }`);
|
||||||
networkPolicyPo.matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
networkPolicyPage.createEditNetworkPolicyForm().matchingNamespacesMessage(1).should('contain.text', `Matches 0 of ${ count }`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can open "Edit as YAML"', () => {
|
it('can open "Edit as YAML"', () => {
|
||||||
NetworkPolicyPagePo.navTo();
|
NetworkPolicyPagePo.navTo();
|
||||||
|
networkPolicyPage.waitForPage();
|
||||||
networkPolicyPage.clickCreate();
|
networkPolicyPage.clickCreate();
|
||||||
const networkPolicyPo = new NetworkPolicyPo();
|
|
||||||
|
|
||||||
networkPolicyPo.editAsYaml().click();
|
networkPolicyPage.createEditNetworkPolicyForm().editAsYaml().click();
|
||||||
networkPolicyPo.yamlEditor().checkExists();
|
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', () => {
|
it('can delete a network policy', () => {
|
||||||
NetworkPolicyPagePo.navTo();
|
NetworkPolicyPagePo.navTo();
|
||||||
networkPolicyPage.waitForPage();
|
networkPolicyPage.waitForPage();
|
||||||
networkPolicyPage.list().actionMenu(networkPolicyName).getMenuItem('Delete').click();
|
networkPolicyPage.list().actionMenu(customNetworkPolicyName).getMenuItem('Delete').click();
|
||||||
const promptRemove = new PromptRemove();
|
const promptRemove = new PromptRemove();
|
||||||
|
|
||||||
cy.intercept('DELETE', 'v1/networking.k8s.io.networkpolicies/**').as('deleteNetworkPolicy');
|
cy.intercept('DELETE', 'v1/networking.k8s.io.networkpolicies/**').as('deleteNetworkPolicy');
|
||||||
promptRemove.remove();
|
promptRemove.remove();
|
||||||
cy.wait('@deleteNetworkPolicy');
|
cy.wait('@deleteNetworkPolicy');
|
||||||
networkPolicyPage.waitForPage();
|
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 { ServicesPagePo } from '@/cypress/e2e/po/pages/explorer/services.po';
|
||||||
import { generateServicesDataSmall, servicesNoData } from '@/cypress/e2e/blueprints/explorer/workloads/service-discovery/services-get';
|
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 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 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'] }, () => {
|
describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login();
|
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'] }, () => {
|
describe('List', { tags: ['@vai', '@adminUser'] }, () => {
|
||||||
|
|
@ -84,12 +218,6 @@ describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }
|
||||||
servicesPagePo.list().resourceTable().sortableTable().checkRowCount(false, 3);
|
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', () => {
|
after('clean up', () => {
|
||||||
cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}');
|
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)
|
// Ensure that the banner should be shown (by confirming that a required repo isn't there)
|
||||||
appRepoList.goTo();
|
appRepoList.goTo();
|
||||||
appRepoList.waitForPage();
|
appRepoList.waitForPage();
|
||||||
|
appRepoList.sortableTable().checkLoadingIndicatorNotVisible();
|
||||||
appRepoList.sortableTable().noRowsShouldNotExist();
|
appRepoList.sortableTable().noRowsShouldNotExist();
|
||||||
appRepoList.sortableTable().rowNames().then((names: any) => {
|
appRepoList.sortableTable().rowNames().then((names: any) => {
|
||||||
if (names.includes(UI_PLUGINS_PARTNERS_REPO_NAME)) {
|
if (names.includes(UI_PLUGINS_PARTNERS_REPO_NAME)) {
|
||||||
|
|
@ -409,6 +410,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
||||||
|
|
||||||
extensionsPo.extensionTabAvailableClick();
|
extensionsPo.extensionTabAvailableClick();
|
||||||
extensionsPo.waitForPage(null, 'available');
|
extensionsPo.waitForPage(null, 'available');
|
||||||
|
extensionsPo.loading().should('not.exist');
|
||||||
|
|
||||||
// Install unauthenticated extension
|
// Install unauthenticated extension
|
||||||
extensionsPo.extensionCardInstallClick(UNAUTHENTICATED_EXTENSION_NAME);
|
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
|
// let's check the extension reload banner and reload the page
|
||||||
extensionsPo.extensionReloadBanner().should('be.visible');
|
extensionsPo.extensionReloadBanner().should('be.visible');
|
||||||
extensionsPo.extensionReloadClick();
|
extensionsPo.extensionReloadClick();
|
||||||
|
extensionsPo.waitForPage(null, 'installed');
|
||||||
|
extensionsPo.loading().should('not.exist');
|
||||||
|
|
||||||
// make sure both extensions have been imported
|
// make sure both extensions have been imported
|
||||||
extensionsPo.extensionScriptImport(UNAUTHENTICATED_EXTENSION_NAME).should('exist');
|
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
|
// make sure both extensions have been imported after logging in again
|
||||||
cy.login(undefined, undefined, false);
|
cy.login(undefined, undefined, false);
|
||||||
extensionsPo.goTo();
|
extensionsPo.goTo();
|
||||||
extensionsPo.waitForPage();
|
extensionsPo.waitForPage(null, 'installed');
|
||||||
|
extensionsPo.loading().should('not.exist');
|
||||||
extensionsPo.waitForTitle();
|
extensionsPo.waitForTitle();
|
||||||
extensionsPo.extensionScriptImport(UNAUTHENTICATED_EXTENSION_NAME).should('exist');
|
extensionsPo.extensionScriptImport(UNAUTHENTICATED_EXTENSION_NAME).should('exist');
|
||||||
extensionsPo.extensionScriptImport(EXTENSION_NAME).should('exist');
|
extensionsPo.extensionScriptImport(EXTENSION_NAME).should('exist');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ describe('Workspaces', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] },
|
||||||
let initialCount: number;
|
let initialCount: number;
|
||||||
|
|
||||||
it('check table headers are available in list and details view', () => {
|
it('check table headers are available in list and details view', () => {
|
||||||
FleetWorkspaceListPagePo.navTo();
|
fleetWorkspacesPage.goTo();
|
||||||
fleetWorkspacesPage.waitForPage();
|
fleetWorkspacesPage.waitForPage();
|
||||||
fleetWorkspacesPage.sortableTable().noRowsShouldNotExist();
|
fleetWorkspacesPage.sortableTable().noRowsShouldNotExist();
|
||||||
fleetWorkspacesPage.sortableTable().filter(defaultWorkspace);
|
fleetWorkspacesPage.sortableTable().filter(defaultWorkspace);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
|
||||||
rke2ClusterAmazon: {
|
rke2ClusterAmazon: {
|
||||||
clusterName: name,
|
clusterName: name,
|
||||||
namespace,
|
namespace,
|
||||||
}
|
},
|
||||||
|
metadata: { labels: { foo: 'bar' } }
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
removeCluster = true;
|
removeCluster = true;
|
||||||
});
|
});
|
||||||
|
|
@ -123,6 +124,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
|
||||||
fleetClusterListPage.clusterList().details(clusterName, 4).should('have.text', '1');
|
fleetClusterListPage.clusterList().details(clusterName, 4).should('have.text', '1');
|
||||||
// check resources: testing https://github.com/rancher/dashboard/issues/11154
|
// check resources: testing https://github.com/rancher/dashboard/issues/11154
|
||||||
fleetClusterListPage.clusterList().details(clusterName, 5).contains( ' 1 ', MEDIUM_TIMEOUT_OPT);
|
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);
|
const fleetClusterDetailsPage = new FleetClusterDetailsPo(namespace, clusterName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,12 +144,18 @@ describe('Performance', { testIsolation: 'off', tags: ['@globalSettings', '@admi
|
||||||
performancePage.garbageCollectionCheckbox().isUnchecked();
|
performancePage.garbageCollectionCheckbox().isUnchecked();
|
||||||
performancePage.garbageCollectionCheckbox().set();
|
performancePage.garbageCollectionCheckbox().set();
|
||||||
performancePage.garbageCollectionCheckbox().isChecked();
|
performancePage.garbageCollectionCheckbox().isChecked();
|
||||||
|
// testing https://github.com/rancher/dashboard/issues/11856
|
||||||
|
performancePage.garbageCollectionResourceCount().set('600');
|
||||||
performancePage.applyAndWait('garbageCollection-true').then(({ request, response }) => {
|
performancePage.applyAndWait('garbageCollection-true').then(({ request, response }) => {
|
||||||
expect(response?.statusCode).to.eq(200);
|
expect(response?.statusCode).to.eq(200);
|
||||||
expect(request.body).to.have.property('value').contains('\"garbageCollection\":{\"enabled\":true');
|
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('\"garbageCollection\":{\"enabled\":true');
|
||||||
|
expect(response?.body).to.have.property('value').contains('\"countThreshold\":600');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check elements value property
|
||||||
|
performancePage.garbageCollectionResourceCount().shouldHaveValue('600');
|
||||||
|
|
||||||
// Disable garbage collection
|
// Disable garbage collection
|
||||||
performancePage.garbageCollectionCheckbox().isChecked();
|
performancePage.garbageCollectionCheckbox().isChecked();
|
||||||
performancePage.garbageCollectionCheckbox().set();
|
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 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 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 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 ClusterManagerNamespacePagePo from '@/cypress/e2e/po/pages/cluster-manager/namespace.po';
|
||||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
@ -563,9 +563,11 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
||||||
|
|
||||||
describe('Imported', { tags: ['@jenkins', '@importedCluster'] }, () => {
|
describe('Imported', { tags: ['@jenkins', '@importedCluster'] }, () => {
|
||||||
const importClusterPage = new ClusterManagerImportGenericPagePo();
|
const importClusterPage = new ClusterManagerImportGenericPagePo();
|
||||||
|
const fqdn = 'fqdn';
|
||||||
|
const cacert = 'cacert';
|
||||||
|
|
||||||
describe('Generic', () => {
|
describe('Generic', () => {
|
||||||
const editImportedClusterPage = new ClusterManagerEditGenericPagePo(undefined, importGenericName);
|
const editImportedClusterPage = new ClusterManagerEditImportedPagePo(undefined, importGenericName);
|
||||||
|
|
||||||
it('can create new cluster', () => {
|
it('can create new cluster', () => {
|
||||||
const detailClusterPage = new ClusterManagerDetailImportedGenericPagePo(undefined, importGenericName);
|
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');
|
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.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();
|
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click();
|
||||||
|
|
||||||
editImportedClusterPage.waitForPage('mode=edit');
|
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', () => {
|
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';
|
import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||||
|
|
||||||
const chartBranch = `release-v${ CURRENT_RANCHER_VERSION }`;
|
const chartBranch = `release-v${ CURRENT_RANCHER_VERSION }`;
|
||||||
|
const gitRepoUrl = 'https://github.com/rancher/charts';
|
||||||
|
|
||||||
describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: ['@manager', '@adminUser'] }, () => {
|
describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: ['@manager', '@adminUser'] }, () => {
|
||||||
const repositoriesPage = new ChartRepositoriesPagePo(undefined, 'manager');
|
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().name().set(this.repoName);
|
||||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
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().gitBranch().set(chartBranch);
|
||||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos').its('response.statusCode').should('eq', 201);
|
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos').its('response.statusCode').should('eq', 201);
|
||||||
repositoriesPage.waitForPage();
|
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().name().set(`${ this.repoName }basic`);
|
||||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
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().gitBranch().set(chartBranch);
|
||||||
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createBasicAuth('test', 'test');
|
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createBasicAuth('test', 'test');
|
||||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos');
|
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().name().set(`${ this.repoName }ssh`);
|
||||||
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
repositoriesPage.createEditRepositories().nameNsDescription().description().set(`${ this.repoName }-description`);
|
||||||
repositoriesPage.createEditRepositories().repoRadioBtn().set(1);
|
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().gitBranch().set(chartBranch);
|
||||||
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createSSHAuth('privateKey', 'publicKey');
|
repositoriesPage.createEditRepositories().clusterRepoAuthSelectOrCreate().createSSHAuth('privateKey', 'publicKey');
|
||||||
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos');
|
repositoriesPage.createEditRepositories().saveAndWaitForRequests('POST', '/v1/catalog.cattle.io.clusterrepos');
|
||||||
|
|
@ -254,4 +255,55 @@ describe('Cluster Management Helm Repositories', { testIsolation: 'off', tags: [
|
||||||
// check list details
|
// check list details
|
||||||
cy.contains(this.repoName).should('not.exist');
|
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,
|
type: string,
|
||||||
clusterName: string,
|
clusterName: string,
|
||||||
namespace: string
|
namespace: string
|
||||||
},
|
},
|
||||||
cloudCredentialsAmazon: {
|
cloudCredentialsAmazon: {
|
||||||
workspace: string,
|
workspace: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -40,7 +40,11 @@ export type CreateAmazonRke2ClusterParams = {
|
||||||
rke2ClusterAmazon: {
|
rke2ClusterAmazon: {
|
||||||
clusterName: string,
|
clusterName: string,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
}
|
},
|
||||||
|
metadata?: {
|
||||||
|
labels?: { [key: string]: string },
|
||||||
|
annotations?: { [key: string]: string },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
export type CreateAmazonRke2ClusterWithoutMachineConfigParams = {
|
export type CreateAmazonRke2ClusterWithoutMachineConfigParams = {
|
||||||
cloudCredentialsAmazon: {
|
cloudCredentialsAmazon: {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ node {
|
||||||
string(credentialsId: 'AWS_SECRET_ACCESS_KEY', variable: 'AWS_SECRET_ACCESS_KEY'),
|
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_AKS_SUBSCRIPTION_ID', variable: 'AZURE_AKS_SUBSCRIPTION_ID'),
|
||||||
string(credentialsId: 'AZURE_CLIENT_ID', variable: 'AZURE_CLIENT_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) {
|
withEnv(paramsMap) {
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
deleteDir()
|
deleteDir()
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ export default defineConfig({
|
||||||
azureClientId: process.env.AZURE_CLIENT_ID,
|
azureClientId: process.env.AZURE_CLIENT_ID,
|
||||||
azureClientSecret: process.env.AZURE_CLIENT_SECRET,
|
azureClientSecret: process.env.AZURE_CLIENT_SECRET,
|
||||||
customNodeIp: process.env.CUSTOM_NODE_IP,
|
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
|
// Jenkins reporters configuration jUnit and HTML
|
||||||
reporter: 'cypress-multi-reporters',
|
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_id "${AZURE_CLIENT_ID}"
|
||||||
corral config vars set azure_client_secret "${AZURE_CLIENT_SECRET}"
|
corral config vars set azure_client_secret "${AZURE_CLIENT_SECRET}"
|
||||||
corral config vars set create_initial_clusters "${CREATE_INITIAL_CLUSTERS}"
|
corral config vars set create_initial_clusters "${CREATE_INITIAL_CLUSTERS}"
|
||||||
|
corral config vars set gke_service_account "${GKE_SERVICE_ACCOUNT}"
|
||||||
|
|
||||||
create_initial_clusters() {
|
create_initial_clusters() {
|
||||||
shopt -u nocasematch
|
shopt -u nocasematch
|
||||||
|
|
|
||||||
|
|
@ -602,7 +602,9 @@ Cypress.Commands.add('deleteNodeTemplate', (nodeTemplateId, timeout = 30000, fai
|
||||||
* Create RKE2 cluster with Amazon EC2 cloud provider
|
* Create RKE2 cluster with Amazon EC2 cloud provider
|
||||||
*/
|
*/
|
||||||
Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2ClusterParams) => {
|
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)
|
return cy.createAwsCloudCredentials(cloudCredentialsAmazon.workspace, cloudCredentialsAmazon.name, cloudCredentialsAmazon.region, cloudCredentialsAmazon.accessKey, cloudCredentialsAmazon.secretKey)
|
||||||
.then((resp: Cypress.Response<any>) => {
|
.then((resp: Cypress.Response<any>) => {
|
||||||
|
|
@ -625,7 +627,11 @@ Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2Cluster
|
||||||
type: 'provisioning.cattle.io.cluster',
|
type: 'provisioning.cattle.io.cluster',
|
||||||
metadata: {
|
metadata: {
|
||||||
namespace: rke2ClusterAmazon.namespace,
|
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
|
name: rke2ClusterAmazon.clusterName
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -76,7 +76,7 @@
|
||||||
"cookie": "0.7.0",
|
"cookie": "0.7.0",
|
||||||
"cookie-universal": "2.2.2",
|
"cookie-universal": "2.2.2",
|
||||||
"cron-validator": "1.2.0",
|
"cron-validator": "1.2.0",
|
||||||
"cronstrue": "1.95.0",
|
"cronstrue": "2.53.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"custom-event-polyfill": "1.0.7",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"d3": "7.3.0",
|
"d3": "7.3.0",
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"file-saver": "2.0.2",
|
"file-saver": "2.0.2",
|
||||||
"floating-vue": "5.2.2",
|
"floating-vue": "5.2.2",
|
||||||
|
"focus-trap": "7.6.2",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
"identicon.js": "2.3.3",
|
"identicon.js": "2.3.3",
|
||||||
"intl-messageformat": "7.8.4",
|
"intl-messageformat": "7.8.4",
|
||||||
|
|
@ -135,7 +136,7 @@
|
||||||
"yaml": "2.5.1"
|
"yaml": "2.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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-methods": "7.18.6",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "7.14.5",
|
"@babel/plugin-proposal-private-property-in-object": "7.14.5",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "7.23.4",
|
"@babel/plugin-transform-nullish-coalescing-operator": "7.23.4",
|
||||||
|
|
@ -178,6 +179,7 @@
|
||||||
"eslint-plugin-cypress": "2.12.1",
|
"eslint-plugin-cypress": "2.12.1",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-jest": "24.4.0",
|
"eslint-plugin-jest": "24.4.0",
|
||||||
|
"eslint-plugin-local-rules": "link:./eslint-plugin-local-rules",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"eslint-plugin-vue": "9.10.0",
|
"eslint-plugin-vue": "9.10.0",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
|
|
@ -188,7 +190,7 @@
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"nodemon": "2.0.22",
|
"nodemon": "2.0.22",
|
||||||
"nyc": "15.1.0",
|
"nyc": "15.1.0",
|
||||||
"start-server-and-test": "1.13.1",
|
"start-server-and-test": "2.0.10",
|
||||||
"style-loader": "3.3.2",
|
"style-loader": "3.3.2",
|
||||||
"ts-jest": "27.1.5",
|
"ts-jest": "27.1.5",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
|
|
@ -197,8 +199,7 @@
|
||||||
"webpack-virtual-modules": "0.4.3",
|
"webpack-virtual-modules": "0.4.3",
|
||||||
"worker-loader": "3.0.8",
|
"worker-loader": "3.0.8",
|
||||||
"yaml-lint": "1.7.0",
|
"yaml-lint": "1.7.0",
|
||||||
"yarn": "1.22.18",
|
"yarn": "1.22.18"
|
||||||
"eslint-plugin-local-rules": "link:./eslint-plugin-local-rules"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"html-webpack-plugin": "^5.0.0"
|
"html-webpack-plugin": "^5.0.0"
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,7 @@ export default defineComponent({
|
||||||
v-if="(taints && taints.length) || isView"
|
v-if="(taints && taints.length) || isView"
|
||||||
class="taints"
|
class="taints"
|
||||||
>
|
>
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<label class="text-label">
|
<label class="text-label">
|
||||||
|
|
@ -407,6 +408,7 @@ export default defineComponent({
|
||||||
</th>
|
</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
<template v-if="taints && taints.length">
|
<template v-if="taints && taints.length">
|
||||||
<Taint
|
<Taint
|
||||||
v-for="(keyedTaint, i) in taints"
|
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.30'];
|
||||||
|
|
||||||
'1.28'];
|
|
||||||
|
|
|
||||||
|
|
@ -388,17 +388,16 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col span-6">
|
<div class="col span-6 mt-20">
|
||||||
<KeyValue
|
<KeyValue
|
||||||
:value="tags"
|
:value="tags"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:title="t('eks.tags.label')"
|
|
||||||
:as-map="true"
|
:as-map="true"
|
||||||
:read-allowed="false"
|
:read-allowed="false"
|
||||||
@update:value="$emit('update:tags', $event)"
|
@update:value="$emit('update:tags', $event)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<label class="text-label">{{ t('eks.tags.label') }}</label>
|
<h3 v-t="'eks.tags.label'" />
|
||||||
</template>
|
</template>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ export default defineComponent({
|
||||||
</ArrayList>
|
</ArrayList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-10">
|
<div class="row mb-10 mt-20">
|
||||||
<div
|
<div
|
||||||
v-if="isNew"
|
v-if="isNew"
|
||||||
class="col span-6"
|
class="col span-6"
|
||||||
|
|
|
||||||
|
|
@ -604,7 +604,7 @@ export default defineComponent({
|
||||||
:label="minMaxDesiredErrors"
|
:label="minMaxDesiredErrors"
|
||||||
/>
|
/>
|
||||||
<div class="row mb-10">
|
<div class="row mb-10">
|
||||||
<div class="col span-6">
|
<div class="col span-6 mt-20">
|
||||||
<KeyValue
|
<KeyValue
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:title="t('eks.nodeGroups.groupLabels.label')"
|
:title="t('eks.nodeGroups.groupLabels.label')"
|
||||||
|
|
@ -614,11 +614,13 @@ export default defineComponent({
|
||||||
@update:value="$emit('update:labels', $event)"
|
@update:value="$emit('update:labels', $event)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<label class="text-label">{{ t('eks.nodeGroups.groupLabels.label') }}</label>
|
<h4>
|
||||||
|
{{ t('eks.nodeGroups.groupLabels.label') }}
|
||||||
|
</h4>
|
||||||
</template>
|
</template>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
<div class="col span-6 mt-20">
|
||||||
<KeyValue
|
<KeyValue
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:title="t('eks.nodeGroups.groupTags.label')"
|
:title="t('eks.nodeGroups.groupTags.label')"
|
||||||
|
|
@ -629,7 +631,7 @@ export default defineComponent({
|
||||||
@update:value="$emit('update:tags', $event)"
|
@update:value="$emit('update:tags', $event)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<label class="text-label">{{ t('eks.nodeGroups.groupTags.label') }}</label>
|
<h4>{{ t('eks.nodeGroups.groupTags.label') }}</h4>
|
||||||
</template>
|
</template>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -812,7 +814,7 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div row="mb-10">
|
<div row="mb-10">
|
||||||
<div class="col span-12">
|
<div class="col span-12 mt-20">
|
||||||
<KeyValue
|
<KeyValue
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
label-key="eks.nodeGroups.resourceTags.label"
|
label-key="eks.nodeGroups.resourceTags.label"
|
||||||
|
|
@ -823,7 +825,9 @@ export default defineComponent({
|
||||||
@update:value="$emit('update:resourceTags', $event)"
|
@update:value="$emit('update:resourceTags', $event)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<label class="text-label">{{ t('eks.nodeGroups.resourceTags.label') }}</label>
|
<h4>
|
||||||
|
{{ t('eks.nodeGroups.resourceTags.label') }}
|
||||||
|
</h4>
|
||||||
</template>
|
</template>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ harvesterManager:
|
||||||
prompt-standard-user: Please contact your system administrator to install the latest Harvester UI Extension, if any
|
prompt-standard-user: Please contact your system administrator to install the latest Harvester UI Extension, if any
|
||||||
missingVersion:
|
missingVersion:
|
||||||
warning: "Could not find a compatible version"
|
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
|
prompt-standard-user: Please contact your system administrator
|
||||||
error:
|
error:
|
||||||
warning: "Warning, Harvester UI extension automatic installation failed"
|
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 { NAME as APP_PRODUCT } from '@shell/config/product/apps';
|
||||||
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
||||||
import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
|
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 {
|
import {
|
||||||
getLatestExtensionVersion,
|
getLatestExtensionVersion,
|
||||||
getHelmRepository,
|
getHelmRepositoryExact,
|
||||||
ensureHelmRepository,
|
getHelmRepositoryMatch,
|
||||||
|
createHelmRepository,
|
||||||
refreshHelmRepository,
|
refreshHelmRepository,
|
||||||
installHelmChart,
|
installHelmChart,
|
||||||
waitForUIExtension,
|
waitForUIExtension,
|
||||||
|
|
@ -102,9 +103,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
async harvesterRepository(value) {
|
async harvesterRepository(neu) {
|
||||||
if (value) {
|
if (neu) {
|
||||||
await refreshHelmRepository(this.$store, HARVESTER_REPO.spec.gitRepo, HARVESTER_REPO.spec.gitBranch);
|
await refreshHelmRepository(this.$store, neu.spec.gitRepo || neu.spec.url);
|
||||||
|
|
||||||
if (this.harvester.extension) {
|
if (this.harvester.extension) {
|
||||||
await this.setHarvesterUpdateVersion();
|
await this.setHarvesterUpdateVersion();
|
||||||
|
|
@ -212,7 +213,11 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async getHarvesterRepository() {
|
async getHarvesterRepository() {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
this.harvesterRepositoryError = true;
|
this.harvesterRepositoryError = true;
|
||||||
}
|
}
|
||||||
|
|
@ -234,13 +239,17 @@ export default {
|
||||||
let installed = false;
|
let installed = false;
|
||||||
|
|
||||||
try {
|
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
|
* Server issue
|
||||||
* It needs to refresh the HelmRepository because the server can have a previous one in the cache.
|
* 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);
|
this.harvesterInstallVersion = await getLatestExtensionVersion(this.$store, HARVESTER_CHART.name, this.rancherVersion, this.kubeVersion);
|
||||||
|
|
||||||
|
|
@ -250,7 +259,16 @@ export default {
|
||||||
return;
|
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);
|
const extension = await waitForUIExtension(this.$store, HARVESTER_CHART.name);
|
||||||
|
|
||||||
|
|
@ -272,7 +290,7 @@ export default {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.harvester.missingRepository) {
|
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();
|
await this.setHarvesterUpdateVersion();
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +301,16 @@ export default {
|
||||||
return;
|
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);
|
const extension = await waitForUIExtension(this.$store, HARVESTER_CHART.name);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { UI_PLUGINS_REPOS } from '@shell/config/uiplugins';
|
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 = {
|
export const HARVESTER_CHART = {
|
||||||
name: 'harvester',
|
name: 'harvester',
|
||||||
version: '',
|
version: '',
|
||||||
|
|
@ -8,21 +13,13 @@ export const HARVESTER_CHART = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HARVESTER_COMMUNITY_REPO = {
|
export const HARVESTER_COMMUNITY_REPO = {
|
||||||
type: 'catalog.cattle.io.clusterrepo',
|
|
||||||
metadata: { name: 'harvester' },
|
metadata: { name: 'harvester' },
|
||||||
spec: {
|
|
||||||
clientSecret: null,
|
|
||||||
gitRepo: 'https://github.com/harvester/harvester-ui-extension',
|
gitRepo: 'https://github.com/harvester/harvester-ui-extension',
|
||||||
gitBranch: 'gh-pages'
|
gitBranch: 'gh-pages',
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HARVESTER_RANCHER_REPO = {
|
export const HARVESTER_RANCHER_REPO = {
|
||||||
type: 'catalog.cattle.io.clusterrepo',
|
|
||||||
metadata: { name: 'rancher' },
|
metadata: { name: 'rancher' },
|
||||||
spec: {
|
|
||||||
clientSecret: null,
|
|
||||||
gitRepo: UI_PLUGINS_REPOS.OFFICIAL.URL,
|
gitRepo: UI_PLUGINS_REPOS.OFFICIAL.URL,
|
||||||
gitBranch: UI_PLUGINS_REPOS.OFFICIAL.BRANCH,
|
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",
|
"babel-eslint": "10.1.0",
|
||||||
"core-js": "3.40.0",
|
"core-js": "3.40.0",
|
||||||
"cron-validator": "1.3.1",
|
"cron-validator": "1.3.1",
|
||||||
"cronstrue": "2.50.0",
|
"cronstrue": "2.53.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"eslint-plugin-promise": "5.2.0",
|
"eslint-plugin-promise": "5.2.0",
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,12 @@ export default defineComponent({
|
||||||
data-testid="accordion-chevron"
|
data-testid="accordion-chevron"
|
||||||
/>
|
/>
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<h4
|
<h2
|
||||||
data-testid="accordion-title-slot-content"
|
data-testid="accordion-title-slot-content"
|
||||||
class="mb-0"
|
class="mb-0"
|
||||||
>
|
>
|
||||||
{{ titleKey ? t(titleKey) : title }}
|
{{ titleKey ? t(titleKey) : title }}
|
||||||
</h4>
|
</h2>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -70,7 +70,7 @@ export default defineComponent({
|
||||||
border: 1px solid var(--border)
|
border: 1px solid var(--border)
|
||||||
}
|
}
|
||||||
.accordion-header {
|
.accordion-header {
|
||||||
padding: 5px;
|
padding: 16px 16px 16px 11px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&>*{
|
&>*{
|
||||||
|
|
@ -81,6 +81,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accordion-body {
|
.accordion-body {
|
||||||
padding: 10px;
|
padding: 0px 16px 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,13 @@ export default defineComponent({
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--success);
|
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>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { createFocusTrap, FocusTrap } from 'focus-trap';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Card',
|
name: 'Card',
|
||||||
|
|
@ -50,12 +51,37 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="cardContainer"
|
||||||
class="card-container"
|
class="card-container"
|
||||||
:class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
|
:class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
|
||||||
data-testid="card"
|
data-testid="card"
|
||||||
|
|
|
||||||
|
|
@ -264,13 +264,15 @@ export default defineComponent({
|
||||||
<template v-else-if="label">{{ label }}</template>
|
<template v-else-if="label">{{ label }}</template>
|
||||||
<i
|
<i
|
||||||
v-if="tooltipKey"
|
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"
|
class="checkbox-info icon icon-info icon-lg"
|
||||||
|
:tabindex="isDisabled ? -1 : 0"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
v-else-if="tooltip"
|
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"
|
class="checkbox-info icon icon-info icon-lg"
|
||||||
|
:tabindex="isDisabled ? -1 : 0"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -329,6 +331,11 @@ $fontColor: var(--input-label);
|
||||||
.checkbox-info {
|
.checkbox-info {
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-custom {
|
.checkbox-custom {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
|
||||||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
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 value = 'any-string';
|
||||||
const delay = 1;
|
const delay = 1;
|
||||||
const wrapper = mount(LabeledInput, {
|
const wrapper = mount(LabeledInput, {
|
||||||
|
|
@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
|
||||||
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
||||||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
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) {
|
if (this.type !== 'cron' || !this.value) {
|
||||||
return;
|
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
|
// refer https://github.com/GuillaumeRochat/cron-validator#readme
|
||||||
if (!isValidCron(this.value as string, {
|
if (!isPredefined && !isValidCron(this.value as string, {
|
||||||
alias: true,
|
alias: true,
|
||||||
allowBlankDay: true,
|
allowBlankDay: true,
|
||||||
allowSevenAsSunday: true,
|
allowSevenAsSunday: true,
|
||||||
})) {
|
})) {
|
||||||
return this.t('generic.invalidCron');
|
return this.t('generic.invalidCron');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
|
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
|
||||||
|
|
||||||
|
|
@ -382,6 +396,7 @@ export default defineComponent({
|
||||||
<div
|
<div
|
||||||
v-if="cronHint || subLabel"
|
v-if="cronHint || subLabel"
|
||||||
class="sub-label"
|
class="sub-label"
|
||||||
|
data-testid="sub-label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="cronHint"
|
v-if="cronHint"
|
||||||
|
|
|
||||||
|
|
@ -243,14 +243,8 @@ $fontColor: var(--input-label);
|
||||||
min-width: 14px;
|
min-width: 14px;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.3s ease-out;
|
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,9 @@ export default defineComponent({
|
||||||
*/
|
*/
|
||||||
isDisabled(): boolean {
|
isDisabled(): boolean {
|
||||||
return (this.disabled || this.isView);
|
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 -->
|
<!-- Group -->
|
||||||
<div
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
:aria-label="radioGroupLabel"
|
||||||
class="radio-group"
|
class="radio-group"
|
||||||
:class="{'row':row}"
|
:class="{'row':row}"
|
||||||
tabindex="0"
|
|
||||||
@keyup.down.stop="clickNext(1)"
|
@keyup.down.stop="clickNext(1)"
|
||||||
@keyup.up.stop="clickNext(-1)"
|
@keyup.up.stop="clickNext(-1)"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,30 @@ INPUT,
|
||||||
SELECT,
|
SELECT,
|
||||||
TEXTAREA,
|
TEXTAREA,
|
||||||
.labeled-input,
|
.labeled-input,
|
||||||
.labeled-select,
|
.checkbox-custom {
|
||||||
.unlabeled-select,
|
|
||||||
.checkbox-custom,
|
|
||||||
.radio-custom {
|
|
||||||
&:focus, &.focused {
|
&: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 {
|
@mixin focus-outline {
|
||||||
// Focus for form like elements (not to be confused with basic :focus style)
|
// 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);
|
outline: 2px solid var(--primary-keyboard-focus);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,11 @@ button,
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-multi-action {
|
.role-multi-action {
|
||||||
|
|
@ -185,6 +190,11 @@ fieldset[disabled] .btn {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
z-index: 1;
|
||||||
|
@include focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@extend .bg-primary;
|
@extend .bg-primary;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ TEXTAREA,
|
||||||
|
|
||||||
@include input-status-color;
|
@include input-status-color;
|
||||||
|
|
||||||
&:focus, &.focused {
|
&:focus:not(.unlabeled-select):not(.labeled-select), &.focused:not(.unlabeled-select):not(.labeled-select) {
|
||||||
@include form-focus
|
@include form-focus;
|
||||||
}
|
}
|
||||||
|
|
||||||
LABEL {
|
LABEL {
|
||||||
|
|
|
||||||
|
|
@ -219,4 +219,8 @@
|
||||||
|
|
||||||
--product-icon : #{$lighter};
|
--product-icon : #{$lighter};
|
||||||
--product-icon-active : #{$lightest};
|
--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 : #{$darker};
|
||||||
--product-icon-active : #{$darkest};
|
--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);
|
color: var(--dropdown-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
z-index: 1000;
|
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 {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ generic:
|
||||||
back: Back
|
back: Back
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
confirm: Confirm
|
confirm: Confirm
|
||||||
|
colorPicker: Color picker
|
||||||
clear: Clear
|
clear: Clear
|
||||||
clearAll: Clear All
|
clearAll: Clear All
|
||||||
close: Close
|
close: Close
|
||||||
|
|
@ -134,6 +135,8 @@ nav:
|
||||||
support: Support page link
|
support: Support page link
|
||||||
about: About page link
|
about: About page link
|
||||||
pinCluster: Pin/Unpin cluster
|
pinCluster: Pin/Unpin cluster
|
||||||
|
collapseExpand: Collapse/Expand menu group
|
||||||
|
productAboutPage: Product about page link
|
||||||
alt:
|
alt:
|
||||||
mainMenuIcon: Main menu icon
|
mainMenuIcon: Main menu icon
|
||||||
mainMenuRancherLogo: Main menu Rancher logo
|
mainMenuRancherLogo: Main menu Rancher logo
|
||||||
|
|
@ -514,6 +517,9 @@ authConfig:
|
||||||
starttls:
|
starttls:
|
||||||
label: Start TLS
|
label: Start TLS
|
||||||
tip: Upgrades non-encrypted connections by wrapping with TLS during the connection process. Can not be used in conjunction with 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
|
tls: TLS
|
||||||
userEnabledAttribute: User Enabled Attribute
|
userEnabledAttribute: User Enabled Attribute
|
||||||
userMemberAttribute: User Member 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.'
|
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.'
|
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:
|
charts:
|
||||||
|
refresh: refresh charts
|
||||||
all: All
|
all: All
|
||||||
categories:
|
categories:
|
||||||
all: All Categories
|
all: All Categories
|
||||||
|
|
@ -952,7 +959,7 @@ catalog:
|
||||||
all: All Operating Systems
|
all: All Operating Systems
|
||||||
linux: Linux
|
linux: Linux
|
||||||
windows: Windows
|
windows: Windows
|
||||||
search: Filter
|
search: Filter charts results
|
||||||
deprecatedChartsFilter:
|
deprecatedChartsFilter:
|
||||||
label: Show deprecated apps
|
label: Show deprecated apps
|
||||||
install:
|
install:
|
||||||
|
|
@ -963,7 +970,7 @@ catalog:
|
||||||
chart: Chart
|
chart: Chart
|
||||||
warning:
|
warning:
|
||||||
managed:
|
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,
|
{version, select,
|
||||||
null { }
|
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.}
|
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)'
|
windows: '{ver} (Windows-only)'
|
||||||
delete:
|
delete:
|
||||||
warning:
|
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:
|
operation:
|
||||||
tableHeaders:
|
tableHeaders:
|
||||||
action: Action
|
action: Action
|
||||||
|
|
@ -2076,6 +2083,7 @@ cluster:
|
||||||
label: Control Plane Concurrency
|
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%)"
|
toolTip: "This can be either a fixed number of nodes (e.g. 1) at a time or a percentage (e.g. 10%)"
|
||||||
workerConcurrency:
|
workerConcurrency:
|
||||||
|
header: Worker Nodes
|
||||||
label: Worker Concurrency
|
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%)"
|
toolTip: "This can be either a fixed number of nodes (e.g. 1) at a time or a percentage (e.g. 10%)"
|
||||||
drain:
|
drain:
|
||||||
|
|
@ -2111,6 +2119,7 @@ cluster:
|
||||||
tlsSan:
|
tlsSan:
|
||||||
label: TLS Alternate Names
|
label: TLS Alternate Names
|
||||||
fqdn:
|
fqdn:
|
||||||
|
label: FQDN
|
||||||
toolTip: A FQDN which will resolve to the healthy control plane nodes of the cluster.
|
toolTip: A FQDN which will resolve to the healthy control plane nodes of the cluster.
|
||||||
caCerts:
|
caCerts:
|
||||||
label: CA Certificates
|
label: CA Certificates
|
||||||
|
|
@ -2383,6 +2392,9 @@ fleet:
|
||||||
cluster:
|
cluster:
|
||||||
summary: Resource Summary
|
summary: Resource Summary
|
||||||
nonReady: Non-Ready Bundles
|
nonReady: Non-Ready Bundles
|
||||||
|
labels: Labels
|
||||||
|
hideLabels: Show less
|
||||||
|
showLabels: Show more
|
||||||
clusters:
|
clusters:
|
||||||
harvester: |-
|
harvester: |-
|
||||||
There {count, plural,
|
There {count, plural,
|
||||||
|
|
@ -6032,6 +6044,8 @@ validation:
|
||||||
flowOutput:
|
flowOutput:
|
||||||
both: Requires "Output" or "Cluster Output" to be selected.
|
both: Requires "Output" or "Cluster Output" to be selected.
|
||||||
global: Requires "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:
|
output:
|
||||||
logdna:
|
logdna:
|
||||||
apiKey: Required an "Api Key" to be set.
|
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'
|
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"
|
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:
|
wizard:
|
||||||
previous: Previous
|
previous: Previous
|
||||||
finish: Finish
|
finish: Finish
|
||||||
|
|
@ -6178,7 +6197,7 @@ wm:
|
||||||
containerShell:
|
containerShell:
|
||||||
clear: Clear
|
clear: Clear
|
||||||
containerName: "Container: {label}"
|
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:
|
logLevel:
|
||||||
info: INFO
|
info: INFO
|
||||||
error: ERROR
|
error: ERROR
|
||||||
|
|
@ -6191,6 +6210,11 @@ wm:
|
||||||
title: "Kubectl: {name}"
|
title: "Kubectl: {name}"
|
||||||
|
|
||||||
workload:
|
workload:
|
||||||
|
scaleWorkloads: Scale workloads
|
||||||
|
healthWorkloads: Jobs/Pods health status
|
||||||
|
healthScaleToggle: Toggle/Expand workloads scaling/health
|
||||||
|
plus: Scale up workload
|
||||||
|
minus: Scale down workload
|
||||||
container:
|
container:
|
||||||
command:
|
command:
|
||||||
addEnvVar: Add Variable
|
addEnvVar: Add Variable
|
||||||
|
|
@ -7434,6 +7458,8 @@ registryConfig:
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
advancedSettings:
|
advancedSettings:
|
||||||
|
setEnv: Set by Environment Variable
|
||||||
|
hideShow: Hide/show setting
|
||||||
label: Settings
|
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'.
|
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
|
show: Show
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ export default {
|
||||||
type="button"
|
type="button"
|
||||||
:class="opt.class"
|
:class="opt.class"
|
||||||
:disabled="disabled || opt.disabled"
|
:disabled="disabled || opt.disabled"
|
||||||
|
role="button"
|
||||||
|
:aria-label="opt.labelKey ? t(opt.labelKey) : opt.label"
|
||||||
@click="change(opt.value)"
|
@click="change(opt.value)"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineEmits } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
defineEmits(['click']);
|
defineEmits(['click']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,36 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="controls"
|
class="controls"
|
||||||
:class="{'disable': sliders.length === 1}"
|
:class="{'disable': sliders.length === 1}"
|
||||||
|
|
@ -204,25 +234,13 @@ export default {
|
||||||
:key="i"
|
:key="i"
|
||||||
class="control-item"
|
class="control-item"
|
||||||
:class="{'active': activeItemId === i}"
|
:class="{'active': activeItemId === i}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="t('carousel.controlItem', { number: i+1 })"
|
||||||
@click="scrollSlide(i, slider.length)"
|
@click="scrollSlide(i, slider.length)"
|
||||||
|
@keyup.enter.space="scrollSlide(i, slider.length)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -240,20 +258,6 @@ export default {
|
||||||
&.disable::after {
|
&.disable::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disable:hover {
|
|
||||||
.prev,
|
|
||||||
.next {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.prev,
|
|
||||||
.next {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-track {
|
.slide-track {
|
||||||
|
|
@ -367,13 +371,16 @@ export default {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
top: 90px;
|
top: 90px;
|
||||||
display: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.disabled .icon {
|
&.disabled .icon {
|
||||||
color: var(--disabled-bg);
|
color: var(--disabled-bg);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.next {
|
.next {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ export default {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -89,7 +93,10 @@ export default {
|
||||||
class="locale-chooser"
|
class="locale-chooser"
|
||||||
>
|
>
|
||||||
{{ selectedLocaleLabel }}
|
{{ selectedLocaleLabel }}
|
||||||
<i class="icon icon-fw icon-sort-down" />
|
<i
|
||||||
|
v-if="showIcon"
|
||||||
|
class="icon icon-fw icon-sort-down"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<ul
|
<ul
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,6 @@
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import ResourceFetch from '@shell/mixins/resource-fetch';
|
import ResourceFetch from '@shell/mixins/resource-fetch';
|
||||||
import ResourceTable from '@shell/components/ResourceTable.vue';
|
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
|
* 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
|
* 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
|
* This will fetch them ALL and will be run in a non-server-side pagination world
|
||||||
|
*
|
||||||
|
* of type PagTableFetchSecondaryResources
|
||||||
*/
|
*/
|
||||||
fetchSecondaryResources: {
|
fetchSecondaryResources: {
|
||||||
type: Function,
|
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
|
* 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
|
* called from shell/mixins/resource-fetch-api-pagination.js
|
||||||
|
*
|
||||||
|
* of type PagTableFetchPageSecondaryResources
|
||||||
*/
|
*/
|
||||||
fetchPageSecondaryResources: {
|
fetchPageSecondaryResources: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,19 @@ export default {
|
||||||
|
|
||||||
return out.filter((obj) => obj.percent);
|
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
|
<div
|
||||||
v-trim-whitespace
|
v-trim-whitespace
|
||||||
:class="{progress: true, multi: pieces.length > 1}"
|
:class="{progress: true, multi: pieces.length > 1}"
|
||||||
|
:aria-label="ariaLabelText"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(piece, idx) of pieces"
|
v-for="(piece, idx) of pieces"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
|
||||||
import { filterBy } from '@shell/utils/array';
|
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||||
|
|
||||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import Question from './Question';
|
import Question from './Question';
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ const LEGACY_MAP = {
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
components: { LabeledInput, LabeledSelect },
|
components: { LabeledInput, ResourceLabeledSelect },
|
||||||
mixins: [Question],
|
mixins: [Question],
|
||||||
|
|
||||||
props: {
|
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() {
|
data() {
|
||||||
const t = this.question.type;
|
const t = this.question.type;
|
||||||
|
|
||||||
|
|
@ -58,28 +53,60 @@ export default {
|
||||||
typeName,
|
typeName,
|
||||||
typeSchema,
|
typeSchema,
|
||||||
all: [],
|
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: {
|
computed: {
|
||||||
isNamespaced() {
|
isNamespaced() {
|
||||||
return !!this.typeSchema?.attributes?.namespaced;
|
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>
|
</script>
|
||||||
|
|
@ -90,15 +117,17 @@ export default {
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledSelect
|
<ResourceLabeledSelect
|
||||||
:mode="mode"
|
:resource-type="typeName"
|
||||||
:options="options"
|
:in-store="inStore"
|
||||||
:disabled="$fetchState.pending || disabled"
|
:disabled="$fetchState.pending || disabled"
|
||||||
:label="displayLabel"
|
:label="displayLabel"
|
||||||
:placeholder="question.description"
|
:placeholder="question.description"
|
||||||
:required="question.required"
|
:required="question.required"
|
||||||
:value="value"
|
:value="value"
|
||||||
:tooltip="displayTooltip"
|
:tooltip="displayTooltip"
|
||||||
|
:paginated-resource-settings="paginateResourceSetting"
|
||||||
|
:all-resources-settings="allResourceSettings"
|
||||||
@update:value="!$fetchState.pending && $emit('update:value', $event)"
|
@update:value="!$fetchState.pending && $emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<router-link
|
||||||
v-if="location"
|
v-if="location"
|
||||||
:to="location"
|
:to="location"
|
||||||
|
role="link"
|
||||||
|
class="masthead-resource-list-link"
|
||||||
|
:aria-label="parent.displayName"
|
||||||
>
|
>
|
||||||
{{ parent.displayName }}:
|
{{ parent.displayName }}:
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -584,10 +587,10 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
HEADER {
|
HEADER {
|
||||||
margin: 0;
|
margin: 0 0 0 -5px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,7 +601,7 @@ export default {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -606,9 +609,13 @@ export default {
|
||||||
.masthead-resource-title {
|
.masthead-resource-title {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.masthead-resource-list-link {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ export default {
|
||||||
:is="asLink ? 'a' : 'div'"
|
:is="asLink ? 'a' : 'div'"
|
||||||
v-for="(r, idx) in rows"
|
v-for="(r, idx) in rows"
|
||||||
:key="get(r, keyField)"
|
: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"
|
:href="asLink ? get(r, linkField) : null"
|
||||||
:target="get(r, targetField)"
|
:target="get(r, targetField)"
|
||||||
:rel="rel"
|
:rel="rel"
|
||||||
|
|
@ -111,9 +115,12 @@ export default {
|
||||||
:data-testid="componentTestid + '-' + get(r, nameField)"
|
:data-testid="componentTestid + '-' + get(r, nameField)"
|
||||||
:class="{
|
:class="{
|
||||||
'has-description': !!get(r, descriptionField),
|
'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)"
|
@click="select(r, idx)"
|
||||||
|
@keyup.enter.space="select(r, idx)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="side-label"
|
class="side-label"
|
||||||
|
|
@ -212,6 +219,10 @@ export default {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
color: $color;
|
color: $color;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:not(.disabled) {
|
&:hover:not(.disabled) {
|
||||||
box-shadow: 0 0 30px var(--shadow);
|
box-shadow: 0 0 30px var(--shadow);
|
||||||
transition: box-shadow 0.1s ease-in-out;
|
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 { TYPE_MODES } from '@shell/store/type-map';
|
||||||
import { NAME as NAVLINKS } from '@shell/config/product/navlinks';
|
import { NAME as NAVLINKS } from '@shell/config/product/navlinks';
|
||||||
import Group from '@shell/components/nav/Group';
|
import Group from '@shell/components/nav/Group';
|
||||||
|
import LocaleSelector from '@shell/components/LocaleSelector';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SideNav',
|
name: 'SideNav',
|
||||||
components: { Group },
|
components: { Group, LocaleSelector },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
groups: [],
|
groups: [],
|
||||||
|
|
@ -112,9 +113,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['managementReady', 'clusterReady']),
|
...mapState(['managementReady', 'clusterReady']),
|
||||||
...mapGetters(['isStandaloneHarvester', 'productId', 'clusterId', 'currentProduct', 'rootProduct', 'isSingleProduct', 'namespaceMode', 'isExplorer', 'isVirtualCluster']),
|
...mapGetters(['isStandaloneHarvester', 'productId', 'clusterId', 'currentProduct', 'rootProduct', 'isSingleProduct', 'namespaceMode', 'isExplorer', 'isVirtualCluster']),
|
||||||
...mapGetters({
|
...mapGetters({ locale: 'i18n/selectedLocaleLabel', hasMultipleLocales: 'i18n/hasMultipleLocales' }),
|
||||||
locale: 'i18n/selectedLocaleLabel', availableLocales: 'i18n/availableLocales', hasMultipleLocales: 'i18n/hasMultipleLocales'
|
|
||||||
}),
|
|
||||||
...mapGetters('type-map', ['activeProducts']),
|
...mapGetters('type-map', ['activeProducts']),
|
||||||
|
|
||||||
favoriteTypes: mapPref(FAVORITE_TYPES),
|
favoriteTypes: mapPref(FAVORITE_TYPES),
|
||||||
|
|
@ -360,10 +359,6 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
switchLocale(locale) {
|
|
||||||
this.$store.dispatch('i18n/switchTo', locale);
|
|
||||||
},
|
|
||||||
|
|
||||||
syncNav() {
|
syncNav() {
|
||||||
const refs = this.$refs.groups;
|
const refs = this.$refs.groups;
|
||||||
|
|
||||||
|
|
@ -427,6 +422,8 @@ export default {
|
||||||
<router-link
|
<router-link
|
||||||
:to="supportLink"
|
:to="supportLink"
|
||||||
class="pull-right"
|
class="pull-right"
|
||||||
|
role="link"
|
||||||
|
:aria-label="t('nav.support', {hasSupport: true})"
|
||||||
>
|
>
|
||||||
{{ t('nav.support', {hasSupport: true}) }}
|
{{ t('nav.support', {hasSupport: true}) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -439,36 +436,11 @@ export default {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- locale selector -->
|
<!-- locale selector -->
|
||||||
<span v-if="isSingleProduct && hasMultipleLocales && !isStandaloneHarvester">
|
<LocaleSelector
|
||||||
<v-dropdown
|
v-if="isSingleProduct && hasMultipleLocales && !isStandaloneHarvester"
|
||||||
popperClass="localeSelector"
|
mode="login"
|
||||||
placement="top"
|
:show-icon="false"
|
||||||
: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>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- SideNav footer alternative -->
|
<!-- SideNav footer alternative -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -478,6 +450,8 @@ export default {
|
||||||
<router-link
|
<router-link
|
||||||
v-if="singleProductAbout"
|
v-if="singleProductAbout"
|
||||||
:to="singleProductAbout"
|
:to="singleProductAbout"
|
||||||
|
role="link"
|
||||||
|
:aria-label="t('nav.ariaLabel.productAboutPage')"
|
||||||
>
|
>
|
||||||
{{ displayVersion }}
|
{{ displayVersion }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,6 @@ export default {
|
||||||
role="tablist"
|
role="tablist"
|
||||||
class="tabs"
|
class="tabs"
|
||||||
:class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
|
:class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
|
||||||
tabindex="0"
|
|
||||||
data-testid="tabbed-block"
|
data-testid="tabbed-block"
|
||||||
@keydown.right.prevent="selectNext(1)"
|
@keydown.right.prevent="selectNext(1)"
|
||||||
@keydown.left.prevent="selectNext(-1)"
|
@keydown.left.prevent="selectNext(-1)"
|
||||||
|
|
@ -277,8 +276,12 @@ export default {
|
||||||
:data-testid="`btn-${tab.name}`"
|
:data-testid="`btn-${tab.name}`"
|
||||||
:aria-controls="'#' + tab.name"
|
:aria-controls="'#' + tab.name"
|
||||||
:aria-selected="tab.active"
|
:aria-selected="tab.active"
|
||||||
|
:aria-label="tab.labelDisplay"
|
||||||
role="tab"
|
role="tab"
|
||||||
|
tabindex="0"
|
||||||
@click.prevent="select(tab.name, $event)"
|
@click.prevent="select(tab.name, $event)"
|
||||||
|
@keyup.enter="select(tab.name, $event)"
|
||||||
|
@keyup.space="select(tab.name, $event)"
|
||||||
>
|
>
|
||||||
<span>{{ tab.labelDisplay }}</span>
|
<span>{{ tab.labelDisplay }}</span>
|
||||||
<span
|
<span
|
||||||
|
|
@ -403,6 +406,14 @@ export default {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.conditions-alert-icon {
|
.conditions-alert-icon {
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,11 @@ export default {
|
||||||
class="name"
|
class="name"
|
||||||
>
|
>
|
||||||
<table>
|
<table>
|
||||||
|
<tbody>
|
||||||
<tr><td>{{ t('principal.name') }}: </td><td>{{ principal.name || principal.loginName }}</td></tr>
|
<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.loginName') }}: </td><td>{{ principal.loginName }}</td></tr>
|
||||||
<tr><td>{{ t('principal.type') }}: </td><td>{{ principal.displayType }}</td></tr>
|
<tr><td>{{ t('principal.type') }}: </td><td>{{ principal.displayType }}</td></tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
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 { STATE, NAME, AGE, FLEET_SUMMARY } from '@shell/config/table-headers';
|
||||||
import { FLEET, MANAGEMENT } from '@shell/config/types';
|
import { FLEET, MANAGEMENT } from '@shell/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ResourceTable },
|
components: { ResourceTable, Tag },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
rows: {
|
rows: {
|
||||||
|
|
@ -75,6 +76,12 @@ export default {
|
||||||
pluralLabel: this.$store.getters['type-map/labelFor'](schema, 99),
|
pluralLabel: this.$store.getters['type-map/labelFor'](schema, 99),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleCustomLabels(row) {
|
||||||
|
row['displayCustomLabels'] = !row.displayCustomLabels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -85,6 +92,7 @@ export default {
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
|
:sub-rows="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
||||||
key-field="_key"
|
key-field="_key"
|
||||||
|
|
@ -123,5 +131,78 @@ export default {
|
||||||
:class="{'text-error': !row.bundleInfo.total}"
|
:class="{'text-error': !row.bundleInfo.total}"
|
||||||
>{{ row.bundleInfo.total }}</span>
|
>{{ row.bundleInfo.total }}</span>
|
||||||
</template>
|
</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>
|
</ResourceTable>
|
||||||
</template>
|
</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 {
|
import {
|
||||||
AGE,
|
AGE,
|
||||||
STATE,
|
|
||||||
NAME,
|
|
||||||
FLEET_SUMMARY,
|
|
||||||
FLEET_REPO,
|
FLEET_REPO,
|
||||||
FLEET_REPO_TARGET,
|
|
||||||
FLEET_REPO_CLUSTERS_READY,
|
|
||||||
FLEET_REPO_CLUSTER_SUMMARY,
|
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';
|
} from '@shell/config/table-headers';
|
||||||
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
|
||||||
|
|
||||||
// i18n-ignore repoDisplay
|
// i18n-ignore repoDisplay
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -77,31 +75,18 @@ export default {
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
// Cluster summary is only shown in the cluster view
|
// Cluster summary is only shown in the cluster view
|
||||||
const fleetClusterSummary = {
|
const summary = this.isClusterView ? [{
|
||||||
...FLEET_REPO_CLUSTER_SUMMARY,
|
...FLEET_REPO_CLUSTER_SUMMARY,
|
||||||
formatterOpts: {
|
formatterOpts: { clusterId: this.clusterId },
|
||||||
// Fleet uses labels to identify clusters
|
}] : [FLEET_REPO_CLUSTERS_READY, FLEET_SUMMARY];
|
||||||
clusterLabel: this.clusterId
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// if hasPerClusterState then use the repo state
|
// if hasPerClusterState then use the repo state
|
||||||
const fleetPerClusterState = {
|
const state = this.isClusterView ? {
|
||||||
...FLEET_REPO_PER_CLUSTER_STATE,
|
...FLEET_REPO_PER_CLUSTER_STATE,
|
||||||
value: (row) => {
|
value: (repo) => repo.clusterState(this.clusterId),
|
||||||
const statePerCluster = row.clusterResourceStatus?.find((c) => {
|
} : STATE;
|
||||||
return c.clusterLabel === this.clusterId;
|
|
||||||
});
|
|
||||||
|
|
||||||
return statePerCluster ? statePerCluster?.status?.displayStatus : STATES_ENUM.ACTIVE;
|
return [
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const summary = this.isClusterView ? [fleetClusterSummary] : [FLEET_REPO_CLUSTERS_READY, FLEET_SUMMARY];
|
|
||||||
|
|
||||||
const state = this.isClusterView ? fleetPerClusterState : STATE;
|
|
||||||
|
|
||||||
const out = [
|
|
||||||
state,
|
state,
|
||||||
NAME,
|
NAME,
|
||||||
FLEET_REPO,
|
FLEET_REPO,
|
||||||
|
|
@ -109,8 +94,6 @@ export default {
|
||||||
...summary,
|
...summary,
|
||||||
AGE
|
AGE
|
||||||
];
|
];
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,21 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
// Ensures that if the default value is used, the model is updated with it
|
// Ensures that if the default value is used, the model is updated with it
|
||||||
this.$emit('update:value', this.inputValue);
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
@ -82,6 +96,8 @@ export default {
|
||||||
class="color-input"
|
class="color-input"
|
||||||
:class="{[mode]:mode, disabled: isDisabled}"
|
:class="{[mode]:mode, disabled: isDisabled}"
|
||||||
:data-testid="componentTestid + '-color-input'"
|
:data-testid="componentTestid + '-color-input'"
|
||||||
|
:tabindex="isDisabled ? -1 : 0"
|
||||||
|
@keyup.enter.space.stop="handleKeyup($event)"
|
||||||
>
|
>
|
||||||
<label class="text-label"><t
|
<label class="text-label"><t
|
||||||
v-if="labelKey"
|
v-if="labelKey"
|
||||||
|
|
@ -99,8 +115,11 @@ export default {
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
|
:aria-disabled="isDisabled ? 'true' : 'false'"
|
||||||
|
:aria-label="t('generic.colorPicker')"
|
||||||
type="color"
|
type="color"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
|
tabindex="-1"
|
||||||
:value="inputValue"
|
:value="inputValue"
|
||||||
@input="$emit('update:value', $event.target.value)"
|
@input="$emit('update:value', $event.target.value)"
|
||||||
>
|
>
|
||||||
|
|
@ -116,6 +135,10 @@ export default {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled, &.disabled .selected, &[disabled], &[disabled]:hover {
|
&.disabled, &.disabled .selected, &[disabled], &[disabled]:hover {
|
||||||
color: var(--input-disabled-text);
|
color: var(--input-disabled-text);
|
||||||
background-color: var(--input-disabled-bg);
|
background-color: var(--input-disabled-bg);
|
||||||
|
|
|
||||||
|
|
@ -798,7 +798,7 @@ export default {
|
||||||
v-if="addAllowed"
|
v-if="addAllowed"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn role-tertiary add"
|
class="btn role-tertiary add"
|
||||||
data-testid="add_link_button"
|
data-testid="add_row_item_button"
|
||||||
:disabled="loading || disabled || (keyOptions && filteredKeyOptions.length === 0)"
|
:disabled="loading || disabled || (keyOptions && filteredKeyOptions.length === 0)"
|
||||||
@click="add()"
|
@click="add()"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -154,11 +154,22 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
// resizeHandler = in mixin
|
// resizeHandler = in mixin
|
||||||
focusSearch() {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
this.$refs['select-input'].open = true;
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const el = this.$refs['select-input']?.searchEl;
|
const el = this.$refs['select-input']?.searchEl;
|
||||||
|
|
@ -278,8 +289,9 @@ export default {
|
||||||
'no-label': !hasLabel
|
'no-label': !hasLabel
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
|
:tabindex="isView || disabled ? -1 : 0"
|
||||||
@click="focusSearch"
|
@click="focusSearch"
|
||||||
@focus="focusSearch"
|
@keyup.enter.space.down="focusSearch"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="{ 'labeled-container': true, raised, empty, [mode]: true }"
|
:class="{ 'labeled-container': true, raised, empty, [mode]: true }"
|
||||||
|
|
@ -319,7 +331,7 @@ export default {
|
||||||
:selectable="selectable"
|
:selectable="selectable"
|
||||||
:modelValue="value != null && !loading ? value : ''"
|
:modelValue="value != null && !loading ? value : ''"
|
||||||
:dropdown-should-open="dropdownShouldOpen"
|
:dropdown-should-open="dropdownShouldOpen"
|
||||||
|
:tabindex="-1"
|
||||||
@update:modelValue="$emit('selecting', $event); $emit('update:value', $event)"
|
@update:modelValue="$emit('selecting', $event); $emit('update:value', $event)"
|
||||||
@search:blur="onBlur"
|
@search:blur="onBlur"
|
||||||
@search:focus="onFocus"
|
@search:focus="onFocus"
|
||||||
|
|
@ -384,7 +396,7 @@ export default {
|
||||||
|
|
||||||
<template #list-footer>
|
<template #list-footer>
|
||||||
<div
|
<div
|
||||||
v-if="canPaginate && totalResults"
|
v-if="canPaginate && totalResults && pages > 1"
|
||||||
class="pagination-slot"
|
class="pagination-slot"
|
||||||
>
|
>
|
||||||
<div class="load-more">
|
<div class="load-more">
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,17 @@ export default {
|
||||||
<button
|
<button
|
||||||
v-clean-tooltip="minusTooltip"
|
v-clean-tooltip="minusTooltip"
|
||||||
:disabled="disabled || !canMinus"
|
:disabled="disabled || !canMinus"
|
||||||
|
:aria-disabled="disabled || !canMinus"
|
||||||
type="button"
|
type="button"
|
||||||
|
role="button"
|
||||||
|
:aria-label="t('workload.plus')"
|
||||||
class="btn btn-sm role-secondary"
|
class="btn btn-sm role-secondary"
|
||||||
@click="$emit('minus')"
|
@click="$emit('minus')"
|
||||||
>
|
>
|
||||||
<i class="icon icon-sm icon-minus" />
|
<i
|
||||||
|
class="icon icon-sm icon-minus"
|
||||||
|
:alt="t('workload.plus')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
|
|
@ -66,11 +72,17 @@ export default {
|
||||||
<button
|
<button
|
||||||
v-clean-tooltip="plusTooltip"
|
v-clean-tooltip="plusTooltip"
|
||||||
:disabled="disabled || !canPlus"
|
:disabled="disabled || !canPlus"
|
||||||
|
:aria-disabled="disabled || !canPlus"
|
||||||
type="button"
|
type="button"
|
||||||
|
role="button"
|
||||||
|
:aria-label="t('workload.minus')"
|
||||||
class="btn btn-sm role-secondary"
|
class="btn btn-sm role-secondary"
|
||||||
@click="$emit('plus')"
|
@click="$emit('plus')"
|
||||||
>
|
>
|
||||||
<i class="icon icon-sm icon-plus" />
|
<i
|
||||||
|
class="icon icon-sm icon-plus"
|
||||||
|
:alt="t('workload.minus')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,8 @@ import { PropType, defineComponent } from 'vue';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||||
import { labelSelectPaginationFunction, LabelSelectPaginationFunctionOptions } from '@shell/components/form/labeled-select-utils/labeled-select.utils';
|
import { labelSelectPaginationFunction, LabelSelectPaginationFunctionOptions } from '@shell/components/form/labeled-select-utils/labeled-select.utils';
|
||||||
import { LabelSelectPaginateFn, LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '@shell/types/components/labeledSelect';
|
import { LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '@shell/types/components/labeledSelect';
|
||||||
|
import { RESOURCE_LABEL_SELECT_MODE, ResourceLabeledSelectPaginateSettings, ResourceLabeledSelectSettings } from '@shell/types/components/resourceLabeledSelect';
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience wrapper around the LabelSelect component to support pagination
|
* Convenience wrapper around the LabelSelect component to support pagination
|
||||||
|
|
@ -66,6 +21,8 @@ export default defineComponent({
|
||||||
|
|
||||||
components: { LabeledSelect },
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* Resource to show
|
* 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: {
|
allResourcesSettings: {
|
||||||
type: Object as PropType<ResourceLabeledSelectSettings>,
|
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: {
|
paginatedResourceSettings: {
|
||||||
type: Object as PropType<ResourceLabeledSelectPaginateSettings>,
|
type: Object as PropType<ResourceLabeledSelectPaginateSettings>,
|
||||||
|
|
@ -120,6 +77,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.paginate) {
|
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 });
|
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);
|
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: {
|
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> {
|
async paginateType(opts: LabelSelectPaginateFnOptions): Promise<LabelSelectPaginateFnResponse> {
|
||||||
if (this.paginatedResourceSettings?.overrideRequest) {
|
if (this.paginatedResourceSettings?.overrideRequest) {
|
||||||
|
|
@ -175,9 +134,9 @@ export default defineComponent({
|
||||||
const options = this.paginatedResourceSettings?.requestSettings ? this.paginatedResourceSettings.requestSettings(defaultOptions) : defaultOptions;
|
const options = this.paginatedResourceSettings?.requestSettings ? this.paginatedResourceSettings.requestSettings(defaultOptions) : defaultOptions;
|
||||||
const res = await labelSelectPaginationFunction(options);
|
const res = await labelSelectPaginationFunction(options);
|
||||||
|
|
||||||
return this.paginatedResourceSettings?.mapResult ? {
|
return this.paginatedResourceSettings?.updateResources ? {
|
||||||
...res,
|
...res,
|
||||||
page: this.paginatedResourceSettings.mapResult(res.page)
|
page: this.paginatedResourceSettings.updateResources(res.page)
|
||||||
} : res;
|
} : res;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -190,5 +149,6 @@ export default defineComponent({
|
||||||
:loading="$fetchState.pending"
|
:loading="$fetchState.pending"
|
||||||
:options="allOfType"
|
:options="allOfType"
|
||||||
:paginate="paginateType"
|
:paginate="paginateType"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
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 }) });
|
const hash = await allHash({ allResources: this.$store.dispatch('cluster/findAll', { type: this.type }) });
|
||||||
|
|
||||||
this.allResources = hash.allResources;
|
this.allResources = hash.allResources;
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,14 @@ import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import Conditions from '@shell/components/form/Conditions';
|
import Conditions from '@shell/components/form/Conditions';
|
||||||
import { EVENT } from '@shell/config/types';
|
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 { _VIEW } from '@shell/config/query-params';
|
||||||
import RelatedResources from '@shell/components/RelatedResources';
|
import RelatedResources from '@shell/components/RelatedResources';
|
||||||
import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class';
|
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 {
|
export default {
|
||||||
|
|
||||||
|
|
@ -21,7 +25,7 @@ export default {
|
||||||
Tabbed,
|
Tabbed,
|
||||||
Tab,
|
Tab,
|
||||||
Conditions,
|
Conditions,
|
||||||
SortableTable,
|
PaginatedResourceTable,
|
||||||
RelatedResources,
|
RelatedResources,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -69,14 +73,25 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const inStore = this.$store.getters['currentStore'](EVENT);
|
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 {
|
return {
|
||||||
hasEvents: this.$store.getters[`${ inStore }/schemaFor`](EVENT), // @TODO be smarter about which resources actually ever have events
|
eventSchema,
|
||||||
allEvents: [],
|
EVENT,
|
||||||
selectedTab: this.defaultTab,
|
selectedTab: this.defaultTab,
|
||||||
didLoadEvents: false,
|
|
||||||
inStore,
|
inStore,
|
||||||
showConditions: false,
|
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: {
|
computed: {
|
||||||
showEvents() {
|
showEvents() {
|
||||||
return this.isView && this.needEvents && this.hasEvents;
|
return this.isView && this.needEvents && this.eventSchema;
|
||||||
},
|
},
|
||||||
showRelated() {
|
showRelated() {
|
||||||
return this.isView && this.needRelated;
|
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() {
|
conditionsHaveIssues() {
|
||||||
if (this.showConditions) {
|
if (this.showConditions) {
|
||||||
return this.value.status?.conditions?.filter((cond) => !isConditionReadyAndWaiting(cond)).some((cond) => cond.error);
|
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
|
// Ensures we only fetch events and show the table when the events tab has been activated
|
||||||
tabChange(neu) {
|
tabChange(neu) {
|
||||||
this.selectedTab = neu?.selectedName;
|
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');
|
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>
|
</script>
|
||||||
|
|
@ -208,15 +250,16 @@ export default {
|
||||||
name="events"
|
name="events"
|
||||||
:weight="-2"
|
:weight="-2"
|
||||||
>
|
>
|
||||||
<SortableTable
|
<!-- namespaced: false given we don't want the default handling of namespaced resource (apply header filter) -->
|
||||||
|
<PaginatedResourceTable
|
||||||
v-if="selectedTab === 'events'"
|
v-if="selectedTab === 'events'"
|
||||||
:rows="events"
|
:schema="eventSchema"
|
||||||
|
:local-filter="filterEventsLocal"
|
||||||
|
:api-filter="filterEventsApi"
|
||||||
|
:use-query-params-for-simple-filtering="false"
|
||||||
:headers="eventHeaders"
|
:headers="eventHeaders"
|
||||||
key-field="id"
|
:paginationHeaders="paginationHeaders"
|
||||||
:search="false"
|
:namespaced="false"
|
||||||
:table-actions="false"
|
|
||||||
:row-actions="false"
|
|
||||||
default-sort-by="date"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default {
|
||||||
secrets: null,
|
secrets: null,
|
||||||
SECRET,
|
SECRET,
|
||||||
allSecretsSettings: {
|
allSecretsSettings: {
|
||||||
mapResult: (secrets) => {
|
updateResources: (secrets) => {
|
||||||
const allSecretsInNamespace = secrets.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
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)));
|
const mappedSecrets = this.mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ export default {
|
||||||
},
|
},
|
||||||
paginateSecretsSetting: {
|
paginateSecretsSetting: {
|
||||||
requestSettings: this.paginatePageOptions,
|
requestSettings: this.paginatePageOptions,
|
||||||
mapResult: (secrets) => {
|
updateResources: (secrets) => {
|
||||||
const mappedSecrets = this.mapSecrets(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
|
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);
|
calculatePosition(dropdownList, component, width, this.placement);
|
||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.focusSearch();
|
|
||||||
},
|
|
||||||
|
|
||||||
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(() => {
|
this.$nextTick(() => {
|
||||||
const el = this.$refs['select-input']?.searchEl;
|
const el = this.$refs['select-input']?.searchEl;
|
||||||
|
|
||||||
|
|
@ -176,6 +189,11 @@ export default {
|
||||||
},
|
},
|
||||||
report(e) {
|
report(e) {
|
||||||
alert(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: {
|
computed: {
|
||||||
|
|
@ -227,7 +245,7 @@ export default {
|
||||||
ref="select"
|
ref="select"
|
||||||
class="unlabeled-select"
|
class="unlabeled-select"
|
||||||
:class="{
|
:class="{
|
||||||
disabled: disabled && !isView,
|
disabled: disabled || isView,
|
||||||
focused,
|
focused,
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
[status]: status,
|
[status]: status,
|
||||||
|
|
@ -236,7 +254,9 @@ export default {
|
||||||
'compact-input': compact,
|
'compact-input': compact,
|
||||||
[$attrs.class]: $attrs.class
|
[$attrs.class]: $attrs.class
|
||||||
}"
|
}"
|
||||||
@focus="focusSearch"
|
:tabindex="disabled || isView ? -1 : 0"
|
||||||
|
@click="focusSearch"
|
||||||
|
@keyup.enter.space.down="focusSearch"
|
||||||
>
|
>
|
||||||
<v-select
|
<v-select
|
||||||
ref="select-input"
|
ref="select-input"
|
||||||
|
|
@ -258,6 +278,8 @@ export default {
|
||||||
:searchable="isSearchable"
|
:searchable="isSearchable"
|
||||||
:selectable="selectable"
|
:selectable="selectable"
|
||||||
:modelValue="value != null ? value : ''"
|
:modelValue="value != null ? value : ''"
|
||||||
|
:dropdownShouldOpen="handleDropdownOpen"
|
||||||
|
:tabindex="-1"
|
||||||
|
|
||||||
@update:modelValue="$emit('update:value', $event)"
|
@update:modelValue="$emit('update:value', $event)"
|
||||||
@search:blur="onBlur"
|
@search:blur="onBlur"
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ describe('component: KeyValue', () => {
|
||||||
expect(secondKeyInput.exists()).toBe(false);
|
expect(secondKeyInput.exists()).toBe(false);
|
||||||
expect(secondValueInput.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');
|
addButton.trigger('click');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
clusterLabel: {
|
clusterId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,6 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<FleetSummaryGraph
|
<FleetSummaryGraph
|
||||||
:row="row"
|
:row="row"
|
||||||
:clusterLabel="clusterLabel"
|
:clusterId="clusterId"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
clusterLabel: {
|
clusterId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
|
|
@ -23,10 +23,8 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
summary() {
|
summary() {
|
||||||
if (this.clusterLabel) {
|
if (this.clusterId) {
|
||||||
return this.row.clusterResourceStatus.find((x) => {
|
return this.row.statusResourceCountsForCluster(this.clusterId);
|
||||||
return x.clusterLabel === this.clusterLabel;
|
|
||||||
})?.status.resourceCounts || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.row.status?.resourceCounts || {};
|
return this.row.status?.resourceCounts || {};
|
||||||
|
|
@ -37,7 +35,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
stateParts() {
|
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 out = keys.map((key) => {
|
||||||
const textColor = colorForState(key);
|
const textColor = colorForState(key);
|
||||||
|
|
@ -46,7 +45,7 @@ export default {
|
||||||
label: ucFirst(key),
|
label: ucFirst(key),
|
||||||
color: textColor.replace(/text-/, 'bg-'),
|
color: textColor.replace(/text-/, 'bg-'),
|
||||||
textColor,
|
textColor,
|
||||||
value: this.summary[key],
|
value: summary[key],
|
||||||
sort: stateSort(textColor, key),
|
sort: stateSort(textColor, key),
|
||||||
};
|
};
|
||||||
}).filter((x) => x.value > 0);
|
}).filter((x) => x.value > 0);
|
||||||
|
|
|
||||||
|
|
@ -182,14 +182,21 @@ export default {
|
||||||
<div
|
<div
|
||||||
id="trigger"
|
id="trigger"
|
||||||
class="hs-popover__trigger"
|
class="hs-popover__trigger"
|
||||||
|
aria-role="button"
|
||||||
|
tabindex="0"
|
||||||
:class="{expanded}"
|
:class="{expanded}"
|
||||||
|
:aria-roledescription="t('workload.scaleWorkloads')"
|
||||||
|
:aria-label="t('workload.healthScaleToggle')"
|
||||||
|
:aria-expanded="expanded"
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
|
@keyup.enter.space="expanded = !expanded"
|
||||||
>
|
>
|
||||||
<ProgressBarMulti
|
<ProgressBarMulti
|
||||||
v-if="parts"
|
v-if="parts"
|
||||||
class="health"
|
class="health"
|
||||||
:values="parts"
|
:values="parts"
|
||||||
:show-zeros="true"
|
:show-zeros="true"
|
||||||
|
:aria-describedby="t('workload.healthWorkloads')"
|
||||||
/>
|
/>
|
||||||
<i :class="{icon: true, 'icon-chevron-up': expanded, 'icon-chevron-down': !expanded}" />
|
<i :class="{icon: true, 'icon-chevron-up': expanded, 'icon-chevron-down': !expanded}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -211,26 +211,40 @@ export default {
|
||||||
v-if="showHeader"
|
v-if="showHeader"
|
||||||
class="header"
|
class="header"
|
||||||
:class="{'active': isOverview, 'noHover': !canCollapse}"
|
:class="{'active': isOverview, 'noHover': !canCollapse}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="group.labelDisplay || group.label || ''"
|
||||||
@click="groupSelected()"
|
@click="groupSelected()"
|
||||||
|
@keyup.enter="groupSelected()"
|
||||||
|
@keyup.space="groupSelected()"
|
||||||
>
|
>
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="hasOverview"
|
v-if="hasOverview"
|
||||||
:to="group.children[0].route"
|
:to="group.children[0].route"
|
||||||
:exact="group.children[0].exact"
|
: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>
|
</router-link>
|
||||||
<h6
|
<h6
|
||||||
v-else
|
v-else
|
||||||
v-clean-html="group.labelDisplay || group.label"
|
>
|
||||||
/>
|
<span v-clean-html="group.labelDisplay || group.label" />
|
||||||
|
</h6>
|
||||||
</slot>
|
</slot>
|
||||||
<i
|
<i
|
||||||
v-if="!onlyHasOverview && canCollapse"
|
v-if="!onlyHasOverview && canCollapse"
|
||||||
class="icon toggle"
|
class="icon toggle toggle-accordion"
|
||||||
:class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
|
:class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="t('nav.ariaLabel.collapseExpand')"
|
||||||
@click="peek($event, true)"
|
@click="peek($event, true)"
|
||||||
|
@keyup.enter="peek($event, true)"
|
||||||
|
@keyup.space="peek($event, true)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
|
|
@ -288,6 +302,7 @@ export default {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--body-text);
|
color: var(--body-text);
|
||||||
height: 33px;
|
height: 33px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
H6 {
|
H6 {
|
||||||
color: var(--body-text);
|
color: var(--body-text);
|
||||||
|
|
@ -315,6 +330,17 @@ export default {
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
.header {
|
.header {
|
||||||
|
&:focus-visible {
|
||||||
|
h6 span {
|
||||||
|
@include focus-outline;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-accordion:focus-visible {
|
||||||
|
@include focus-outline;
|
||||||
|
outline-offset: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--primary-hover-text);
|
color: var(--primary-hover-text);
|
||||||
background-color: var(--primary-hover-bg);
|
background-color: var(--primary-hover-bg);
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,10 @@ export default {
|
||||||
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
||||||
</TabTitle>
|
</TabTitle>
|
||||||
<a
|
<a
|
||||||
|
role="link"
|
||||||
|
:aria-label="type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label)"
|
||||||
:href="href"
|
:href="href"
|
||||||
|
class="type-link"
|
||||||
@click="selectType(); navigate($event);"
|
@click="selectType(); navigate($event);"
|
||||||
@mouseenter="setNear(true)"
|
@mouseenter="setNear(true)"
|
||||||
@mouseleave="setNear(false)"
|
@mouseleave="setNear(false)"
|
||||||
|
|
@ -161,9 +164,11 @@ export default {
|
||||||
data-testid="link-type"
|
data-testid="link-type"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
role="link"
|
||||||
:href="type.link"
|
:href="type.link"
|
||||||
:target="type.target"
|
:target="type.target"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
|
:aria-label="type.label"
|
||||||
>
|
>
|
||||||
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -178,6 +183,16 @@ export default {
|
||||||
margin-right: 4px;
|
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 {
|
.child {
|
||||||
margin: 0 var(--outline) 0 0;
|
margin: 0 var(--outline) 0 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,8 @@ export const FLEET = {
|
||||||
CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name',
|
CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name',
|
||||||
CLUSTER_NAME: 'management.cattle.io/cluster-name',
|
CLUSTER_NAME: 'management.cattle.io/cluster-name',
|
||||||
BUNDLE_ID: 'fleet.cattle.io/bundle-id',
|
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',
|
MANAGED: 'fleet.cattle.io/managed',
|
||||||
CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
|
CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
|
||||||
CLUSTER: 'fleet.cattle.io/cluster'
|
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