mirror of https://github.com/rancher/dashboard.git
Merge branch 'master' of github.com:rancher/dashboard into 12485-repositories-disabling-feature
This commit is contained in:
commit
face94e4ff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
|
|||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
||||
});
|
||||
|
||||
it('using mode "multiline" should emit input value correctly', () => {
|
||||
it('using type "multiline" should emit input value correctly', () => {
|
||||
const value = 'any-string';
|
||||
const delay = 1;
|
||||
const wrapper = mount(LabeledInput, {
|
||||
|
|
@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
|
|||
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
||||
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
||||
});
|
||||
|
||||
describe('using type "chron"', () => {
|
||||
it.each([
|
||||
['0 * * * *', 'Every hour, every day'],
|
||||
['@daily', 'At 12:00 AM, every day'],
|
||||
['You must fail! Go!', '%generic.invalidCron%'],
|
||||
])('passing value %p should display hint %p', (value, hint) => {
|
||||
const wrapper = mount(LabeledInput, {
|
||||
propsData: { value, type: 'cron' },
|
||||
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
|
||||
});
|
||||
|
||||
const subLabel = wrapper.find('[data-testid="sub-label"]');
|
||||
|
||||
expect(subLabel.text()).toBe(hint);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -179,14 +179,28 @@ export default defineComponent({
|
|||
if (this.type !== 'cron' || !this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO - #13202: This is required due use of 2 libraries and 3 different libraries through the code.
|
||||
const predefined = [
|
||||
'@yearly',
|
||||
'@annually',
|
||||
'@monthly',
|
||||
'@weekly',
|
||||
'@daily',
|
||||
'@midnight',
|
||||
'@hourly'
|
||||
];
|
||||
const isPredefined = predefined.includes(this.value as string);
|
||||
|
||||
// refer https://github.com/GuillaumeRochat/cron-validator#readme
|
||||
if (!isValidCron(this.value as string, {
|
||||
if (!isPredefined && !isValidCron(this.value as string, {
|
||||
alias: true,
|
||||
allowBlankDay: true,
|
||||
allowSevenAsSunday: true,
|
||||
})) {
|
||||
return this.t('generic.invalidCron');
|
||||
}
|
||||
|
||||
try {
|
||||
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
|
||||
|
||||
|
|
@ -382,6 +396,7 @@ export default defineComponent({
|
|||
<div
|
||||
v-if="cronHint || subLabel"
|
||||
class="sub-label"
|
||||
data-testid="sub-label"
|
||||
>
|
||||
<div
|
||||
v-if="cronHint"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import Tag from '@shell/components/Tag.vue';
|
||||
import { STATE, NAME, AGE, FLEET_SUMMARY } from '@shell/config/table-headers';
|
||||
import { FLEET, MANAGEMENT } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
components: { ResourceTable },
|
||||
components: { ResourceTable, Tag },
|
||||
|
||||
props: {
|
||||
rows: {
|
||||
|
|
@ -75,6 +76,12 @@ export default {
|
|||
pluralLabel: this.$store.getters['type-map/labelFor'](schema, 99),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleCustomLabels(row) {
|
||||
row['displayCustomLabels'] = !row.displayCustomLabels;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -85,6 +92,7 @@ export default {
|
|||
:schema="schema"
|
||||
:headers="headers"
|
||||
:rows="rows"
|
||||
:sub-rows="true"
|
||||
:loading="loading"
|
||||
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
||||
key-field="_key"
|
||||
|
|
@ -123,5 +131,78 @@ export default {
|
|||
:class="{'text-error': !row.bundleInfo.total}"
|
||||
>{{ row.bundleInfo.total }}</span>
|
||||
</template>
|
||||
|
||||
<template #sub-row="{fullColspan, row, onRowMouseEnter, onRowMouseLeave}">
|
||||
<tr
|
||||
class="labels-row sub-row"
|
||||
@mouseenter="onRowMouseEnter"
|
||||
@mouseleave="onRowMouseLeave"
|
||||
>
|
||||
<template v-if="row.customLabels.length">
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td :colspan="fullColspan-2">
|
||||
<span
|
||||
v-if="row.customLabels.length"
|
||||
class="mt-5"
|
||||
> {{ t('fleet.cluster.labels') }}:
|
||||
<span
|
||||
v-for="(label, i) in row.customLabels"
|
||||
:key="i"
|
||||
class="mt-5 labels"
|
||||
>
|
||||
<Tag
|
||||
v-if="i < 7"
|
||||
class="mr-5 label"
|
||||
>
|
||||
{{ label }}
|
||||
</Tag>
|
||||
<Tag
|
||||
v-else-if="i > 6 && row.displayCustomLabels"
|
||||
class="mr-5 label"
|
||||
>
|
||||
{{ label }}
|
||||
</Tag>
|
||||
</span>
|
||||
<a
|
||||
v-if="row.customLabels.length > 7"
|
||||
href="#"
|
||||
@click.prevent="toggleCustomLabels(row)"
|
||||
>
|
||||
{{ t(`fleet.cluster.${row.displayCustomLabels? 'hideLabels' : 'showLabels'}`) }}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
<td
|
||||
v-else
|
||||
:colspan="fullColspan"
|
||||
>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.labels-row {
|
||||
td {
|
||||
padding-top:0;
|
||||
.tag {
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.labels {
|
||||
display: inline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
13
yarn.lock
13
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue