Merge branch 'master' of github.com:rancher/dashboard into 12485-repositories-disabling-feature

This commit is contained in:
Mo Mesgin 2025-01-30 10:43:25 -08:00
commit face94e4ff
19 changed files with 276 additions and 47 deletions

View File

@ -7,4 +7,8 @@ export default class FleetClusterList extends BaseResourceList {
details(name: string, index: number) {
return this.resourceTable().sortableTable().rowWithName(name).column(index);
}
subRows() {
return this.resourceTable().sortableTable().subRows();
}
}

View File

@ -70,7 +70,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
rke2ClusterAmazon: {
clusterName: name,
namespace,
}
},
metadata: { labels: { foo: 'bar' } }
}).then(() => {
removeCluster = true;
});
@ -123,6 +124,8 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => {
fleetClusterListPage.clusterList().details(clusterName, 4).should('have.text', '1');
// check resources: testing https://github.com/rancher/dashboard/issues/11154
fleetClusterListPage.clusterList().details(clusterName, 5).contains( ' 1 ', MEDIUM_TIMEOUT_OPT);
// check cluster labels
fleetClusterListPage.clusterList().subRows().should('contain.text', 'foo=bar');
const fleetClusterDetailsPage = new FleetClusterDetailsPo(namespace, clusterName);

View File

@ -29,7 +29,7 @@ export type CreateAmazonRke2ClusterParams = {
type: string,
clusterName: string,
namespace: string
},
},
cloudCredentialsAmazon: {
workspace: string,
name: string,
@ -40,7 +40,11 @@ export type CreateAmazonRke2ClusterParams = {
rke2ClusterAmazon: {
clusterName: string,
namespace: string,
}
},
metadata?: {
labels?: { [key: string]: string },
annotations?: { [key: string]: string },
},
}
export type CreateAmazonRke2ClusterWithoutMachineConfigParams = {
cloudCredentialsAmazon: {

View File

@ -602,7 +602,9 @@ Cypress.Commands.add('deleteNodeTemplate', (nodeTemplateId, timeout = 30000, fai
* Create RKE2 cluster with Amazon EC2 cloud provider
*/
Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2ClusterParams) => {
const { machineConfig, rke2ClusterAmazon, cloudCredentialsAmazon } = params;
const {
machineConfig, rke2ClusterAmazon, cloudCredentialsAmazon, metadata
} = params;
return cy.createAwsCloudCredentials(cloudCredentialsAmazon.workspace, cloudCredentialsAmazon.name, cloudCredentialsAmazon.region, cloudCredentialsAmazon.accessKey, cloudCredentialsAmazon.secretKey)
.then((resp: Cypress.Response<any>) => {
@ -625,8 +627,12 @@ Cypress.Commands.add('createAmazonRke2Cluster', (params: CreateAmazonRke2Cluster
type: 'provisioning.cattle.io.cluster',
metadata: {
namespace: rke2ClusterAmazon.namespace,
annotations: { 'field.cattle.io/description': `${ rke2ClusterAmazon.clusterName }-description` },
name: rke2ClusterAmazon.clusterName
annotations: {
'field.cattle.io/description': `${ rke2ClusterAmazon.clusterName }-description`,
...(metadata?.annotations || {}),
},
labels: metadata?.labels || {},
name: rke2ClusterAmazon.clusterName
},
spec: {
rkeConfig: {

View File

@ -76,7 +76,7 @@
"cookie": "0.7.0",
"cookie-universal": "2.2.2",
"cron-validator": "1.2.0",
"cronstrue": "1.95.0",
"cronstrue": "2.53.0",
"cross-env": "7.0.3",
"custom-event-polyfill": "1.0.7",
"d3": "7.3.0",

View File

@ -36,7 +36,7 @@
"babel-eslint": "10.1.0",
"core-js": "3.40.0",
"cron-validator": "1.3.1",
"cronstrue": "2.50.0",
"cronstrue": "2.53.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "5.2.0",

View File

@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
});
it('using mode "multiline" should emit input value correctly', () => {
it('using type "multiline" should emit input value correctly', () => {
const value = 'any-string';
const delay = 1;
const wrapper = mount(LabeledInput, {
@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
expect(wrapper.emitted('update:value')).toHaveLength(1);
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
});
describe('using type "chron"', () => {
it.each([
['0 * * * *', 'Every hour, every day'],
['@daily', 'At 12:00 AM, every day'],
['You must fail! Go!', '%generic.invalidCron%'],
])('passing value %p should display hint %p', (value, hint) => {
const wrapper = mount(LabeledInput, {
propsData: { value, type: 'cron' },
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
});
const subLabel = wrapper.find('[data-testid="sub-label"]');
expect(subLabel.text()).toBe(hint);
});
});
});

View File

@ -179,14 +179,28 @@ export default defineComponent({
if (this.type !== 'cron' || !this.value) {
return;
}
// TODO - #13202: This is required due use of 2 libraries and 3 different libraries through the code.
const predefined = [
'@yearly',
'@annually',
'@monthly',
'@weekly',
'@daily',
'@midnight',
'@hourly'
];
const isPredefined = predefined.includes(this.value as string);
// refer https://github.com/GuillaumeRochat/cron-validator#readme
if (!isValidCron(this.value as string, {
if (!isPredefined && !isValidCron(this.value as string, {
alias: true,
allowBlankDay: true,
allowSevenAsSunday: true,
})) {
return this.t('generic.invalidCron');
}
try {
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
@ -382,6 +396,7 @@ export default defineComponent({
<div
v-if="cronHint || subLabel"
class="sub-label"
data-testid="sub-label"
>
<div
v-if="cronHint"

View File

@ -2385,6 +2385,9 @@ fleet:
cluster:
summary: Resource Summary
nonReady: Non-Ready Bundles
labels: Labels
hideLabels: Show less
showLabels: Show more
clusters:
harvester: |-
There {count, plural,

View File

@ -1,10 +1,11 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Tag from '@shell/components/Tag.vue';
import { STATE, NAME, AGE, FLEET_SUMMARY } from '@shell/config/table-headers';
import { FLEET, MANAGEMENT } from '@shell/config/types';
export default {
components: { ResourceTable },
components: { ResourceTable, Tag },
props: {
rows: {
@ -75,6 +76,12 @@ export default {
pluralLabel: this.$store.getters['type-map/labelFor'](schema, 99),
};
},
},
methods: {
toggleCustomLabels(row) {
row['displayCustomLabels'] = !row.displayCustomLabels;
}
}
};
</script>
@ -85,6 +92,7 @@ export default {
:schema="schema"
:headers="headers"
:rows="rows"
:sub-rows="true"
:loading="loading"
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
key-field="_key"
@ -123,5 +131,78 @@ export default {
:class="{'text-error': !row.bundleInfo.total}"
>{{ row.bundleInfo.total }}</span>
</template>
<template #sub-row="{fullColspan, row, onRowMouseEnter, onRowMouseLeave}">
<tr
class="labels-row sub-row"
@mouseenter="onRowMouseEnter"
@mouseleave="onRowMouseLeave"
>
<template v-if="row.customLabels.length">
<td>&nbsp;</td>
<td>&nbsp;</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"
>
&nbsp;
</td>
</tr>
</template>
</ResourceTable>
</template>
<style lang='scss' scoped>
.labels-row {
td {
padding-top:0;
.tag {
margin-right: 5px;
display: inline-block;
margin-top: 2px;
}
}
}
.labels {
display: inline;
flex-wrap: wrap;
.label {
display: inline-block;
margin-top: 2px;
}
}
</style>

View File

@ -1,5 +1,5 @@
import { LOCAL_CLUSTER, MANAGEMENT, NORMAN } from '@shell/config/types';
import { CAPI, FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
import { CAPI, FLEET as FLEET_LABELS, SYSTEM_LABELS } from '@shell/config/labels-annotations';
import { _RKE2 } from '@shell/store/prefs';
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
@ -190,6 +190,22 @@ export default class FleetCluster extends SteveModel {
}
}
get customLabels() {
const parsedLabels = [];
if (this.labels) {
for (const k in this.labels) {
const [prefix] = k.split('/');
if (!SYSTEM_LABELS.includes(prefix) && k !== CAPI.PROVIDER) {
parsedLabels.push(`${ k }=${ this.labels[k] }`);
}
}
}
return parsedLabels;
}
async saveYaml(yaml) {
await this._saveYaml(yaml);

View File

@ -64,7 +64,7 @@
"cookie": "0.7.0",
"core-js": "3.40.0",
"cron-validator": "1.3.1",
"cronstrue": "2.50.0",
"cronstrue": "2.53.0",
"cross-env": "7.0.3",
"css-loader": "6.7.3",
"csv-loader": "3.0.3",

View File

@ -432,7 +432,8 @@ class StevePaginationUtils extends NamespaceProjectFilters {
// Check if the API supports filtering by this field
this.validateField(validateFields, schema, field.field);
const exactPartial = field.exact ? `'${ field.value }'` : field.value;
const value = encodeURIComponent(field.value);
const exactPartial = field.exact ? `'${ value }'` : value;
return `${ this.convertArrayPath(field.field) }${ field.equals ? '=' : '!=' }${ exactPartial }`;
}

View File

@ -2,8 +2,13 @@ import cronstrue from 'cronstrue';
export function cronSchedule(schedule = '', getters, errors) {
try {
cronstrue.toString(schedule, { verbose: true });
cronScheduleRule.validation(schedule);
} catch (e) {
errors.push(getters['i18n/t']('validation.invalidCron'));
errors.push(getters['i18n/t'](cronScheduleRule.message));
}
}
export const cronScheduleRule = {
validation: (text) => cronstrue.toString(text, { verbose: true }),
message: 'validation.invalidCron'
};

View File

@ -27,22 +27,6 @@ describe('formRules', () => {
expect(formRuleResult).toStrictEqual(expectedResult);
});
it('"cronSchedule" : returns undefined when valid cron string value supplied', () => {
const testValue = '0 * * * *';
const formRuleResult = formRules.cronSchedule(testValue);
expect(formRuleResult).toBeUndefined();
});
it('"cronSchedule" : returns the correct message when invalid cron string value supplied', () => {
// specific logic of what constitutes a cron string is in the "cronstrue" function in an external library and not tested here
const testValue = '0 * * **';
const formRuleResult = formRules.cronSchedule(testValue);
const expectedResult = JSON.stringify({ message: 'validation.invalidCron' });
expect(formRuleResult).toStrictEqual(expectedResult);
});
it('"https" : returns undefined when valid https url value is supplied', () => {
const testValue = 'https://url.com';
const formRuleResult = formRules.https(testValue);
@ -1112,6 +1096,13 @@ describe('formRules', () => {
expect(formRuleResult).toStrictEqual(expectedResult);
});
/**
* Test all factory validators
* @param rule - the name of the factory validator
* @param argument - the value to validate
* @param correctValues - an array of values that should pass the validation
* @param wrongValues - an array of values that should fail the validation
*/
describe.each([
['minValue', 2, [3], [1]],
['maxValue', 256, [1], [300]],
@ -1133,12 +1124,18 @@ describe('formRules', () => {
});
});
/**
* Test all standard validators
* @param rule - the name of the standard validator
* @param correctValues - an array of values that should pass the validation
* @param wrongValues - an array of values that should fail the validation
*/
describe.each([
['requiredInt', [2, 2.2], ['e']],
['isInteger', ['2', 2, 0], [2.2, 'e', '1.0']],
['isPositive', ['0', 1], [-1]],
['isOctal', ['0', 0, 10], ['01']],
['cronSchedule', ['0 * * * *', '@daily'], ['0 * * **']],
])('given validator %p', (rule, correctValues, wrongValues) => {
it.each(wrongValues as [])('should return error for value %p', (wrong) => {
const formRuleResult = (formRules as any)[rule](wrong);

View File

@ -4,14 +4,26 @@ import isEmpty from 'lodash/isEmpty';
import has from 'lodash/has';
import isUrl from 'is-url';
// import uniq from 'lodash/uniq';
import cronstrue from 'cronstrue';
import { Translation } from '@shell/types/t';
import { isHttps, isLocalhost, hasTrailingForwardSlash } from '@shell/utils/validators/setting';
import { cronScheduleRule } from '@shell/utils/validators/cron-schedule';
// import uniq from 'lodash/uniq';
export type Validator<T = undefined | string> = (val: any, arg?: any) => T;
export type ValidatorFactory = (arg1: any, arg2?: any) => Validator
/**
* Fixed validation rule which require only the value to be evaluated
* @param value
* @returns { string | undefined }
*/
export type Validator<T = undefined | string> = (value: any, arg?: any) => T;
/**
* Factory function which returns a validation rule
* @param arg Argument used as part of the validation rule process, not necessarily as parameter of the validation rule
* @param value Value to be evaluated
* @returns { Validator }
*/
export type ValidatorFactory = (arg: any, value?: any) => Validator
type ServicePort = {
name?: string,
@ -131,9 +143,9 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
const cronSchedule: Validator = (val: string) => {
try {
cronstrue.toString(val, { verbose: true });
cronScheduleRule.validation(val);
} catch (e) {
return t('validation.invalidCron');
return t(cronScheduleRule.message);
}
};

View File

@ -34,3 +34,23 @@ Input elements with type text allow users to enter any combination of lett
#### Error
<Canvas of={LabeledInput.Error} />
#### SubLabel
Addition information can be added to the input field using the `subLabel` prop.
<Canvas of={LabeledInput.SubLabel} />
#### Cron Type
The `cron` prop can be used to add [Cron language](https://en.wikipedia.org/wiki/Cron) hints.
<Canvas of={LabeledInput.CronType} />
<Canvas of={LabeledInput.CronTypeDaily} />
<Canvas of={LabeledInput.CronTypeError} />
#### Multiline Type
<Canvas of={LabeledInput.MultilineType} />

View File

@ -64,3 +64,53 @@ export const Error: Story = {
tooltipKey: 'Error message'
},
};
export const SubLabel: Story = {
...Default,
args: {
label: 'Name',
subLabel: 'Additional information',
type: 'text',
value: 'Simon',
},
};
export const CronType: Story = {
...Default,
args: {
label: 'Period',
type: 'cron',
value: '0 * * * *',
},
};
export const CronTypeDaily: Story = {
...Default,
args: {
label: 'Period',
type: 'cron',
value: '@daily',
},
};
export const CronTypeError: Story = {
...Default,
args: {
label: 'Period',
type: 'cron',
value: 'not a cron expression',
},
};
export const MultilineType: Story = {
...Default,
args: {
label: 'Period',
type: 'multiline',
value: `this
is
a
multiline
text`,
},
};

View File

@ -6386,15 +6386,10 @@ cron-validator@1.3.1:
resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e"
integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==
cronstrue@1.95.0:
version "1.95.0"
resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-1.95.0.tgz#171df1fad8b0f0cb636354dd1d7842161c15478f"
integrity sha512-CdbQ17Z8Na2IdrK1SiD3zmXfE66KerQZ8/iApkGsxjmUVGJPS9M9oK4FZC3LM6ohUjjq3UeaSk+90Cf3QbXDfw==
cronstrue@2.50.0:
version "2.50.0"
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573"
integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==
cronstrue@2.53.0:
version "2.53.0"
resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-2.53.0.tgz#5bbcd7483636b99379480f624faef5056f3efbd8"
integrity sha512-CkAcaI94xL8h6N7cGxgXfR5D7oV2yVtDzB9vMZP8tIgPyEv/oc/7nq9rlk7LMxvc3N+q6LKZmNLCVxJRpyEg8A==
cross-env@7.0.3:
version "7.0.3"