diff --git a/docs/cluster-workflow-templates.md b/docs/cluster-workflow-templates.md new file mode 100644 index 000000000000..5ce1d5f2f5c1 --- /dev/null +++ b/docs/cluster-workflow-templates.md @@ -0,0 +1,81 @@ +# Cluster Workflow Templates + +> v2.8 and after + +## Introduction + +`ClusterWorkflowTemplates` are cluster scoped `WorkflowTemplates`. `ClusterWorkflowTemplate` +can be created cluster scoped like `ClusterRole` and can be accessed all namespaces in the cluster. + +`WorkflowTemplate` documentation [link](./workflow-template.md) + +## Defining `ClusterWorkflowTemplate` + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ClusterWorkflowTemplate +metadata: + name: cluster-workflow-template-whalesay-template +spec: + templates: + - name: whalesay-template + inputs: + parameters: + - name: message + container: + image: docker/whalesay + command: [cowsay] + args: ["{{inputs.parameters.message}}"] +``` + +## Referencing other `ClusterWorkflowTemplates` + +You can reference `templates` from another `ClusterWorkflowTemplates` using a `templateRef` field with `clusterScope: true` . +Just as how you reference other `templates` within the same `Workflow`, you should do so from a `steps` or `dag` template. + +Here is an example: +More examples []() +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + entrypoint: whalesay + templates: + - name: whalesay + steps: # You should only reference external "templates" in a "steps" or "dag" "template". + - - name: call-whalesay-template + templateRef: # You can reference a "template" from another "WorkflowTemplate or ClusterWorkflowTemplate" using this field + name: cluster-workflow-template-whalesay-template # This is the name of the "WorkflowTemplate or ClusterWorkflowTempalte" CRD that contains the "template" you want + template: whalesay-template # This is the name of the "template" you want to reference + clusterScope: true # This field indicates this templateRef is pointing ClusterWorkflowTemplate + arguments: # You can pass in arguments as normal + parameters: + - name: message + value: "hello world" +``` + +## Managing `ClusterWorkflowTemplates` + +### CLI + +You can create some example templates as follows: + +``` +argo cluster-template create https://raw.githubusercontent.com/argoproj/argo/master/examples/cluster-workflow-template/clustertemplates.yaml +``` + +The submit a workflow using one of those templates: + +``` +argo submit https://raw.githubusercontent.com/argoproj/argo/master/examples/cluster-workflow-template/cluster-wftmpl-dag.yaml +``` + +### `kubectl` + +Using `kubectl apply -f` and `kubectl get cwft` + +### UI + +`ClusterWorkflowTemplate` resources can also be managed by the UI diff --git a/go.mod b/go.mod index f37d250d816c..8e2745369e9b 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/tools v0.0.0-20200401192744-099440627f01 // indirect + golang.org/x/tools v0.0.0-20200402223321-bcf690261a44 // indirect google.golang.org/api v0.20.0 google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24 google.golang.org/grpc v1.28.0 diff --git a/go.sum b/go.sum index 25d33ebaa9dd..195433611c97 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= @@ -613,8 +614,8 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200401192744-099440627f01 h1:ysQJ/fU6laLOZJseIeOqXl6Mo+lw5z6b7QHnmUKjW+k= -golang.org/x/tools v0.0.0-20200401192744-099440627f01/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200402223321-bcf690261a44 h1:bMm0eoDiGkM5VfIyKjxDvoflW5GLp7+VCo+60n8F+TE= +golang.org/x/tools v0.0.0-20200402223321-bcf690261a44/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ui/src/app/app.tsx b/ui/src/app/app.tsx index 24c5a78785be..f610ff13f3c8 100644 --- a/ui/src/app/app.tsx +++ b/ui/src/app/app.tsx @@ -9,6 +9,7 @@ import {ContextApis, Provider} from './shared/context'; import archivedWorkflows from './archived-workflows'; import cronWorkflows from './cron-workflows'; +import clusterWorkflowTemplates from './cluster-workflow-templates' import help from './help'; import login from './login'; import ErrorBoundary from './shared/components/error-boundary'; @@ -17,6 +18,8 @@ import workflows from './workflows'; const workflowsUrl = uiUrl('workflows'); const workflowTemplatesUrl = uiUrl('workflow-templates'); +const clusterWorkflowTemplatesUrl = uiUrl('cluster-workflow-templates'); + const cronWorkflowUrl = uiUrl('cron-workflows'); const archivedWorkflowUrl = uiUrl('archived-workflows'); const helpUrl = uiUrl('help'); @@ -27,6 +30,7 @@ const routes: { } = { [workflowsUrl]: {component: workflows.component}, [workflowTemplatesUrl]: {component: workflowTemplates.component}, + [clusterWorkflowTemplatesUrl]: {component: clusterWorkflowTemplates.component}, [cronWorkflowUrl]: {component: cronWorkflows.component}, [archivedWorkflowUrl]: {component: archivedWorkflows.component}, [helpUrl]: {component: help.component}, @@ -44,6 +48,11 @@ const navItems = [ { title: 'Workflow Templates', path: workflowTemplatesUrl, + iconClassName: 'fas fa-object-group' + }, + { + title: 'Cluster Workflow Templates', + path: clusterWorkflowTemplatesUrl, iconClassName: 'fa fa-clone' }, { diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-container.tsx b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-container.tsx new file mode 100644 index 000000000000..ef18b0fd8178 --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-container.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import {Route, RouteComponentProps, Switch} from 'react-router'; +import {ClusterWorkflowTemplateDetails} from './cluster-workflow-template-details/cluster-workflow-template-details'; +import {ClusterWorkflowTemplateList} from './cluster-workflow-template-list/cluster-workflow-template-list'; + +export const ClusterWorkflowTemplateContainer = (props: RouteComponentProps) => ( + + + + +); diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx new file mode 100644 index 000000000000..c3bc39c73015 --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-details/cluster-workflow-template-details.tsx @@ -0,0 +1,147 @@ +import {NotificationType, Page} from 'argo-ui'; +import {SlidingPanel} from 'argo-ui/src/index'; +import * as React from 'react'; +import {RouteComponentProps} from 'react-router'; +import * as models from '../../../../models'; +import {uiUrl} from '../../../shared/base'; +import {BasePage} from '../../../shared/components/base-page'; +import {Loading} from '../../../shared/components/loading'; +import {ResourceSubmit} from '../../../shared/components/resource-submit'; +import {Consumer} from '../../../shared/context'; +import {services} from '../../../shared/services'; +import {ClusterWorkflowTemplateSummaryPanel} from '../cluster-workflow-template-summary-panel'; + +require('../../../workflows/components/workflow-details/workflow-details.scss'); + +interface State { + template?: models.ClusterWorkflowTemplate; + error?: Error; +} + +export class ClusterWorkflowTemplateDetails extends BasePage, State> { + + + private get name() { + return this.props.match.params.name; + } + + private get sidePanel() { + return this.queryParam('sidePanel'); + } + + private set sidePanel(sidePanel) { + this.setQueryParams({sidePanel}); + } + + constructor(props: RouteComponentProps, context: any) { + super(props, context); + this.state = {}; + } + + public componentDidMount(): void { + services.clusterWorkflowTemplate + .get(this.name) + .then(template => { + this.setState({template}); + }) + .catch(error => this.setState({error})); + } + + public render() { + if (this.state.error !== undefined) { + throw this.state.error; + } + return ( + + {ctx => ( + (this.sidePanel = 'new') + }, + { + title: 'Delete', + iconClassName: 'fa fa-trash', + action: () => this.deleteClusterWorkflowTemplate() + } + ] + }, + breadcrumbs: [ + { + title: 'Cluster Workflow Template', + path: uiUrl('cluster-workflow-templates') + }, + {title: this.name} + ] + }}> +
+
{this.renderClusterWorkflowTemplate()}
+
+ {this.state.template && ( + (this.sidePanel = null)}> + + resourceName={'Workflow'} + defaultResource={this.getWorkflow(this.state.template)} + onSubmit={wfValue => { + return services.workflows + .create(wfValue, wfValue.metadata.namespace) + .then(workflow => ctx.navigation.goto(uiUrl(`workflows/${workflow.metadata.namespace}/${workflow.metadata.name}`))); + }} + /> + + )} +
+ )} +
+ ); + } + + private renderClusterWorkflowTemplate() { + if (!this.state.template) { + return ; + } + return this.setState({template})} onError={error => this.setState({error})} />; + } + + private deleteClusterWorkflowTemplate() { + if (!confirm('Are you sure you want to delete this cluster workflow template?\nThere is no undo.')) { + return; + } + services.clusterWorkflowTemplate + .delete(this.name) + .catch(e => { + this.appContext.apis.notifications.show({ + content: 'Failed to delete cluster workflow template ' + e, + type: NotificationType.Error + }); + }) + .then(() => { + document.location.href = uiUrl('cluster-workflow-templates'); + }); + } + + private getWorkflow(template: models.ClusterWorkflowTemplate): models.Workflow { + return { + metadata: { + generateName: template.metadata.name + '-', + namespace: '' + }, + spec: { + entrypoint: template.spec.templates[0].name, + templates: template.spec.templates.map(t => ({ + name: t.name, + templateRef: { + name: template.metadata.name, + template: t.name, + clusterscope: true, + } + })) + } + }; + } +} diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.scss b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.scss new file mode 100644 index 000000000000..2293c1f83a06 --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.scss @@ -0,0 +1,8 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.argo-table-list { + margin: 1em; + &__row:hover { + box-shadow: 1px 2px 3px rgba($argo-color-gray-9, .1), 0 0 0 1px rgba($argo-color-teal-5, .5); + } +} \ No newline at end of file diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.tsx b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.tsx new file mode 100644 index 000000000000..9af46eab5e60 --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-list/cluster-workflow-template-list.tsx @@ -0,0 +1,140 @@ +import {Page, SlidingPanel} from 'argo-ui'; +import * as React from 'react'; +import {Link, RouteComponentProps} from 'react-router-dom'; +import * as models from '../../../../models'; +import {uiUrl} from '../../../shared/base'; +import {BasePage} from '../../../shared/components/base-page'; +import {Loading} from '../../../shared/components/loading'; +import {ResourceSubmit} from '../../../shared/components/resource-submit'; +import {Timestamp} from '../../../shared/components/timestamp'; +import {ZeroState} from '../../../shared/components/zero-state'; +import {Consumer} from '../../../shared/context'; +import {exampleClusterWorkflowTemplate} from '../../../shared/examples'; +import {services} from '../../../shared/services'; +import {Utils} from '../../../shared/utils'; + +require('./cluster-workflow-template-list.scss'); + +interface State { + loading: boolean; + namespace: string; + templates?: models.ClusterWorkflowTemplate[]; + error?: Error; +} + +export class ClusterWorkflowTemplateList extends BasePage, State> { + + private get sidePanel() { + return this.queryParam('sidePanel'); + } + + private set sidePanel(sidePanel) { + this.setQueryParams({sidePanel}); + } + + constructor(props: RouteComponentProps, context: any) { + super(props, context); + this.state = {loading: true, namespace: this.props.match.params.namespace || Utils.getCurrentNamespace() || ''}; + } + + public componentDidMount(): void { + this.fetchClusterWorkflowTemplates(); + } + + public render() { + if (this.state.loading) { + return ; + } + if (this.state.error) { + throw this.state.error; + } + return ( + + {ctx => ( + (this.sidePanel = 'new') + } + ] + }, + }}> + {this.renderTemplates()} + (this.sidePanel = null)}> + + resourceName={'Cluster Workflow Template'} + defaultResource={exampleClusterWorkflowTemplate()} + onSubmit={wfTmpl => { + return services.clusterWorkflowTemplate + .create(wfTmpl) + .then(wf => ctx.navigation.goto(uiUrl(`cluster-workflow-templates/${wf.metadata.name}`))); + }} + /> + + + )} + + ); + } + + private fetchClusterWorkflowTemplates(): void { + services.info + .get() + .then(info => { + return services.clusterWorkflowTemplate.list(); + }) + .then(templates => this.setState({templates, loading: false})) + .catch(error => this.setState({error, loading: false})); + } + + private renderTemplates() { + if (!this.state.templates) { + return ; + } + const learnMore = Learn more; + if (this.state.templates.length === 0) { + return ( + +

You can create new templates here or using the CLI.

+

{learnMore}.

+
+ ); + } + return ( +
+
+
+
+
+
NAME
+
CREATED
+
+ {this.state.templates.map(t => ( + +
+ +
+
{t.metadata.name}
+
+ +
+ + ))} +
+

+ Cluster scoped Workflow templates are reusable templates you can create new workflows from. {learnMore}. +

+
+
+ ); + } +} diff --git a/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-summary-panel.tsx b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-summary-panel.tsx new file mode 100644 index 000000000000..fb6917fb02a8 --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/components/cluster-workflow-template-summary-panel.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import {WorkflowTemplate} from '../../../models'; +import {Timestamp} from '../../shared/components/timestamp'; +import {YamlEditor} from '../../shared/components/yaml/yaml-editor'; +import {services} from '../../shared/services'; + +interface Props { + template: WorkflowTemplate; + onChange: (template: WorkflowTemplate) => void; + onError: (error: Error) => void; +} + +export const ClusterWorkflowTemplateSummaryPanel = (props: Props) => { + const attributes = [ + {title: 'Name', value: props.template.metadata.name}, + {title: 'Created', value: } + ]; + return ( +
+
+
+ {attributes.map(attr => ( +
+
{attr.title}
+
{attr.value}
+
+ ))} +
+
+ +
+
+ { + return services.clusterWorkflowTemplate + .update(value, props.template.metadata.name) + .then(clusterWorkflowTemplate => props.onChange(clusterWorkflowTemplate)) + .catch(err => props.onError(err)); + }} + /> +
+
+
+ ); +}; diff --git a/ui/src/app/cluster-workflow-templates/index.ts b/ui/src/app/cluster-workflow-templates/index.ts new file mode 100644 index 000000000000..c6f11005312e --- /dev/null +++ b/ui/src/app/cluster-workflow-templates/index.ts @@ -0,0 +1,5 @@ +import {ClusterWorkflowTemplateContainer} from './components/cluster-workflow-template-container'; + +export default { + component: ClusterWorkflowTemplateContainer +}; diff --git a/ui/src/app/shared/components/resource-submit.tsx b/ui/src/app/shared/components/resource-submit.tsx index 2baf2b2c0550..4cafe939c1fd 100644 --- a/ui/src/app/shared/components/resource-submit.tsx +++ b/ui/src/app/shared/components/resource-submit.tsx @@ -52,7 +52,7 @@ export class ResourceSubmit extends React.Component, R

)} { const adjectives = ['wonderful', 'fantastic', 'awesome', 'delightful', 'lovely']; @@ -29,6 +29,25 @@ export const exampleWorkflow = (namespace: string): Workflow => ({ ] } }); +export const exampleClusterWorkflowTemplate = (): ClusterWorkflowTemplate => ({ + metadata: { + name: randomSillyName(), + }, + spec: { + templates: [ + { + name: 'whalesay', + container: { + name: 'main', + image: 'docker/whalesay:latest', + command: ['cowsay'], + args: ['hello world'] + } + } + ] + } +}); + export const exampleWorkflowTemplate = (namespace: string): WorkflowTemplate => ({ metadata: { @@ -49,6 +68,7 @@ export const exampleWorkflowTemplate = (namespace: string): WorkflowTemplate => ] } }); + export const exampleCronWorkflow = (namespace: string): CronWorkflow => ({ metadata: { name: randomSillyName(), diff --git a/ui/src/app/shared/services/cluster-workflow-template-service.ts b/ui/src/app/shared/services/cluster-workflow-template-service.ts new file mode 100644 index 000000000000..8dde0e86fbae --- /dev/null +++ b/ui/src/app/shared/services/cluster-workflow-template-service.ts @@ -0,0 +1,33 @@ +import * as models from '../../../models'; +import requests from './requests'; + +export class ClusterWorkflowTemplateService { + public create(template: models.ClusterWorkflowTemplate) { + return requests + .post(`api/v1/cluster-workflow-templates`) + .send({template}) + .then(res => res.body as models.ClusterWorkflowTemplate); + } + + public list() { + return requests + .get(`api/v1/cluster-workflow-templates`) + .then(res => res.body as models.ClusterWorkflowTemplateList) + .then(list => list.items || []); + } + + public get(name: string) { + return requests.get(`api/v1/cluster-workflow-templates/${name}`).then(res => res.body as models.ClusterWorkflowTemplate); + } + + public update(template: models.ClusterWorkflowTemplate, name: string) { + return requests + .put(`api/v1/cluster-workflow-templates/${name}`) + .send({template}) + .then(res => res.body as models.ClusterWorkflowTemplate); + } + + public delete(name: string) { + return requests.delete(`api/v1/cluster-workflow-templates/${name}`); + } +} diff --git a/ui/src/app/shared/services/index.ts b/ui/src/app/shared/services/index.ts index 25584360d770..fc5041888f6b 100644 --- a/ui/src/app/shared/services/index.ts +++ b/ui/src/app/shared/services/index.ts @@ -2,12 +2,14 @@ import {ArchivedWorkflowsService} from './archived-workflows-service'; import {CronWorkflowService} from './cron-workflow-service'; import {InfoService} from './info-service'; import {WorkflowTemplateService} from './workflow-template-service'; +import {ClusterWorkflowTemplateService} from './cluster-workflow-template-service'; import {WorkflowsService} from './workflows-service'; export interface Services { info: InfoService; workflows: WorkflowsService; workflowTemplate: WorkflowTemplateService; + clusterWorkflowTemplate: ClusterWorkflowTemplateService; archivedWorkflows: ArchivedWorkflowsService; cronWorkflows: CronWorkflowService; } @@ -19,6 +21,7 @@ export const services: Services = { info: new InfoService(), workflows: new WorkflowsService(), workflowTemplate: new WorkflowTemplateService(), + clusterWorkflowTemplate: new ClusterWorkflowTemplateService(), archivedWorkflows: new ArchivedWorkflowsService(), cronWorkflows: new CronWorkflowService() }; diff --git a/ui/src/models/cluster-workflow-templates.ts b/ui/src/models/cluster-workflow-templates.ts new file mode 100644 index 000000000000..3d5fc264adce --- /dev/null +++ b/ui/src/models/cluster-workflow-templates.ts @@ -0,0 +1,16 @@ +import * as kubernetes from 'argo-ui/src/models/kubernetes'; +import {WorkflowSpec} from './workflows'; + +export interface ClusterWorkflowTemplate { + apiVersion?: string; + kind?: string; + metadata: kubernetes.ObjectMeta; + spec: WorkflowSpec; +} + +export interface ClusterWorkflowTemplateList { + apiVersion?: string; + kind?: string; + metadata: kubernetes.ListMeta; + items: ClusterWorkflowTemplate[]; +} diff --git a/ui/src/models/index.ts b/ui/src/models/index.ts index c3724e1bafb7..ff6e6a296061 100644 --- a/ui/src/models/index.ts +++ b/ui/src/models/index.ts @@ -2,4 +2,5 @@ export * from './info'; export * from './workflows'; export * from './workflow-templates'; export * from './cron-workflows'; +export * from './cluster-workflow-templates' export {models as kubernetes} from 'argo-ui'; diff --git a/ui/yarn.lock b/ui/yarn.lock index ef2c7b44a27b..a1965793c5bb 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1534,9 +1534,9 @@ dashdash@^1.12.0: assert-plus "^1.0.0" date-fns@^2.0.1: - version "2.11.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.0.tgz#ec2b44977465b9dcb370021d5e6c019b19f36d06" - integrity sha512-8P1cDi8ebZyDxUyUprBXwidoEtiQAawYPGvpfb+Dg0G6JrQ+VozwOmm91xYC0vAv1+0VmLehEPb+isg4BGUFfA== + version "2.11.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.1.tgz#197b8be1bbf5c5e6fe8bea817f0fe111820e7a12" + integrity sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w== date-now@^0.1.4: version "0.1.4" @@ -4571,9 +4571,9 @@ react-autocomplete@^1.8.1: prop-types "^15.5.10" react-datepicker@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.14.0.tgz#cdb2f236f120a7e6e973b617f71358b5c8617aac" - integrity sha512-9TUDNj0zoeQT3ey6i7Dv4NLcqONyYqXNEOLA3++HwQKR5NK4eRoG4QaohM/5XmWw2tDpJWpl3ByCWP4kWQtqgQ== + version "2.14.1" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.14.1.tgz#83463beb85235a575475955f554290a95f89c65b" + integrity sha512-8eWHvrjXfKVkt5rERXC6/c/eEdcE2stIsl+QmTO5Efgpacf8MOCyVpBisJLVLDYjVlENczhOcRlIzvraODHKxA== dependencies: classnames "^2.2.6" date-fns "^2.0.1"