@spinnaker/docker 2025.0.6 → 2025.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,735 @@
1
+ import { groupBy, reduce, trim, uniq } from 'lodash';
2
+ import React from 'react';
3
+ import type { Option } from 'react-select';
4
+ import Select from 'react-select';
5
+
6
+ import type { IAccount, IFindImageParams } from '@spinnaker/core';
7
+ import { AccountService, HelpField, Tooltip, ValidationMessage } from '@spinnaker/core';
8
+
9
+ import type { IDockerImage } from './DockerImageReader';
10
+ import { DockerChartImageReader } from './DockerImageReader';
11
+ import type { IDockerImageParts } from './DockerImageUtils';
12
+ import { DockerImageUtils } from './DockerImageUtils';
13
+
14
+ export type IDockerLookupType = 'tag' | 'digest';
15
+
16
+ export interface IDockerChartAndTagChanges {
17
+ account?: string;
18
+ organization?: string;
19
+ registry?: string;
20
+ repository?: string;
21
+ tag?: string;
22
+ digest?: string;
23
+ imageId?: string;
24
+ }
25
+
26
+ export interface IDockerChartAndTagSelectorProps {
27
+ specifyTagByRegex: boolean;
28
+ imageId: string;
29
+ organization: string;
30
+ registry: string;
31
+ repository: string;
32
+ tag: string;
33
+ digest: string;
34
+ account: string;
35
+ showRegistry?: boolean;
36
+ onChange: (changes: IDockerChartAndTagChanges) => void;
37
+ deferInitialization?: boolean;
38
+ showDigest?: boolean;
39
+ allowManualDefinition?: boolean;
40
+ }
41
+
42
+ export interface IDockerChartAndTagSelectorState {
43
+ accountOptions: Array<Option<string>>;
44
+ switchedManualWarning: string;
45
+ missingFields?: string[];
46
+ imagesLoaded: boolean;
47
+ imagesLoading: boolean;
48
+ organizationOptions: Array<Option<string>>;
49
+ repositoryOptions: Array<Option<string>>;
50
+ defineManually: boolean;
51
+ tagOptions: Array<Option<string>>;
52
+ lookupType: IDockerLookupType;
53
+ }
54
+
55
+ const imageFields = ['organization', 'repository', 'tag', 'digest'];
56
+ const defineOptions = [
57
+ { label: 'Manually', value: true },
58
+ { label: 'Select from list', value: false },
59
+ ];
60
+
61
+ export class DockerChartAndTagSelector extends React.Component<
62
+ IDockerChartAndTagSelectorProps,
63
+ IDockerChartAndTagSelectorState
64
+ > {
65
+ public static defaultProps: Partial<IDockerChartAndTagSelectorProps> = {
66
+ organization: '',
67
+ registry: '',
68
+ repository: '',
69
+ showDigest: true,
70
+ allowManualDefinition: true,
71
+ };
72
+
73
+ private unmounted = false;
74
+ private images: IDockerImage[];
75
+ private accounts: string[];
76
+
77
+ private registryMap: { [key: string]: string };
78
+ private accountMap: { [key: string]: string[] };
79
+ private newAccounts: string[];
80
+ private organizationMap: { [key: string]: string[] };
81
+ private repositoryMap: { [key: string]: string[] };
82
+ private organizations: string[];
83
+ private cachedValues: { [key: string]: string } = {};
84
+
85
+ public constructor(props: IDockerChartAndTagSelectorProps) {
86
+ super(props);
87
+
88
+ const accountOptions = props.account ? [{ label: props.account, value: props.account }] : [];
89
+ const organizationOptions =
90
+ props.organization && props.organization.length ? [{ label: props.organization, value: props.organization }] : [];
91
+ const repositoryOptions =
92
+ props.repository && props.repository.length ? [{ label: props.repository, value: props.repository }] : [];
93
+ const tagOptions = props.tag && props.tag.length ? [{ label: props.tag, value: props.tag }] : [];
94
+ const parsedImageId = DockerImageUtils.splitImageId(props.imageId);
95
+ const defineManually = props.allowManualDefinition && Boolean(props.imageId && props.imageId.includes('${'));
96
+
97
+ this.state = {
98
+ accountOptions,
99
+ switchedManualWarning: undefined,
100
+ imagesLoaded: false,
101
+ imagesLoading: false,
102
+ organizationOptions,
103
+ repositoryOptions,
104
+ defineManually,
105
+ tagOptions,
106
+ lookupType: props.digest || parsedImageId.digest ? 'digest' : 'tag',
107
+ };
108
+ }
109
+
110
+ private getAccountMap(images: IDockerImage[]): { [key: string]: string[] } {
111
+ const groupedImages = groupBy(
112
+ images.filter((image) => image.account),
113
+ 'account',
114
+ );
115
+ return reduce<IDockerImage[], { [key: string]: string[] }>(
116
+ groupedImages,
117
+ (acc, image, key) => {
118
+ acc[key] = uniq(image.map((i) => `${i.repository.split('/').slice(0, -1).join('/')}`));
119
+ return acc;
120
+ },
121
+ {},
122
+ );
123
+ }
124
+
125
+ private getRegistryMap(images: IDockerImage[]) {
126
+ return images.reduce((m: { [key: string]: string }, image: IDockerImage) => {
127
+ m[image.account] = image.registry;
128
+ return m;
129
+ }, {} as { [key: string]: string });
130
+ }
131
+
132
+ private getOrganizationMap(images: IDockerImage[]): { [key: string]: string[] } {
133
+ const extractGroupByKey = (image: IDockerImage) =>
134
+ `${image.account}/${image.repository.split('/').slice(0, -1).join('/')}`;
135
+ const groupedImages = groupBy(
136
+ images.filter((image) => image.repository),
137
+ extractGroupByKey,
138
+ );
139
+ return reduce<IDockerImage[], { [key: string]: string[] }>(
140
+ groupedImages,
141
+ (acc, image, key) => {
142
+ acc[key] = uniq(image.map((i) => i.repository));
143
+ return acc;
144
+ },
145
+ {},
146
+ );
147
+ }
148
+
149
+ private getRepositoryMap(images: IDockerImage[]) {
150
+ const groupedImages = groupBy(
151
+ images.filter((image) => image.account),
152
+ 'repository',
153
+ );
154
+ return reduce<IDockerImage[], { [key: string]: string[] }>(
155
+ groupedImages,
156
+ (acc, image, key) => {
157
+ acc[key] = uniq(image.map((i) => i.tag));
158
+ return acc;
159
+ },
160
+ {},
161
+ );
162
+ }
163
+
164
+ private getOrganizationsList(accountMap: { [key: string]: string[] }) {
165
+ return accountMap ? accountMap[this.props.showRegistry ? this.props.account : this.props.registry] || [] : [];
166
+ }
167
+
168
+ private getRepositoryList(organizationMap: { [key: string]: string[] }, organization: string, registry: string) {
169
+ if (organizationMap) {
170
+ const key = `${this.props.showRegistry ? this.props.account : registry}/${organization || ''}`;
171
+ return organizationMap[key] || [];
172
+ }
173
+ return [];
174
+ }
175
+
176
+ private getTags(tag: string, repositoryMap: { [key: string]: string[] }, repository: string) {
177
+ let tags: string[] = [];
178
+ if (this.props.specifyTagByRegex) {
179
+ if (tag && trim(tag) === '') {
180
+ tag = undefined;
181
+ }
182
+ } else {
183
+ if (repositoryMap) {
184
+ tags = repositoryMap[repository] || [];
185
+ if (!tags.includes(tag) && tag && !tag.includes('${')) {
186
+ tag = undefined;
187
+ }
188
+ }
189
+ }
190
+
191
+ return { tag, tags };
192
+ }
193
+
194
+ public componentWillReceiveProps(nextProps: IDockerChartAndTagSelectorProps) {
195
+ if (
196
+ !this.images ||
197
+ ['account', 'showRegistry'].some(
198
+ (key: keyof IDockerChartAndTagSelectorProps) => this.props[key] !== nextProps[key],
199
+ )
200
+ ) {
201
+ this.refreshImages(nextProps);
202
+ } else if (
203
+ ['organization', 'registry', 'repository'].some(
204
+ (key: keyof IDockerChartAndTagSelectorProps) => this.props[key] !== nextProps[key],
205
+ )
206
+ ) {
207
+ this.updateThings(nextProps);
208
+ }
209
+
210
+ if (nextProps.imageId && nextProps.imageId.includes('${')) {
211
+ this.setState({ defineManually: true });
212
+ }
213
+ }
214
+
215
+ componentWillUnmount() {
216
+ this.unmounted = true;
217
+ }
218
+
219
+ private synchronizeChanges(values: IDockerImageParts, registry: string) {
220
+ const { organization, repository, tag, digest } = values;
221
+ if (this.props.onChange) {
222
+ const imageId = DockerImageUtils.generateImageId({ organization, repository, tag, digest });
223
+ const changes: IDockerChartAndTagChanges = {};
224
+ if (tag !== this.props.tag) {
225
+ changes.tag = tag;
226
+ }
227
+ if (imageId !== this.props.imageId) {
228
+ changes.imageId = imageId;
229
+ }
230
+ if (organization !== this.props.organization) {
231
+ changes.organization = organization;
232
+ }
233
+ if (registry !== this.props.registry) {
234
+ changes.registry = registry;
235
+ }
236
+ if (repository !== this.props.repository) {
237
+ changes.repository = repository;
238
+ }
239
+ if (digest !== this.props.digest) {
240
+ changes.digest = digest;
241
+ }
242
+ if (Object.keys(changes).length > 0) {
243
+ this.props.onChange(changes);
244
+ }
245
+ }
246
+ }
247
+
248
+ private updateThings(props: IDockerChartAndTagSelectorProps, allowAutoSwitchToManualEntry = false) {
249
+ if (!this.repositoryMap || this.unmounted) {
250
+ return;
251
+ }
252
+
253
+ const { imageId, specifyTagByRegex } = props;
254
+ let { organization, registry, repository } = props;
255
+
256
+ if (props.showRegistry) {
257
+ registry = this.registryMap[props.account];
258
+ }
259
+
260
+ const organizationFound = !organization || this.organizations.includes(organization) || organization.includes('${');
261
+ if (!organizationFound) {
262
+ organization = '';
263
+ }
264
+
265
+ const repositories = this.getRepositoryList(this.organizationMap, organization, registry);
266
+ const repositoryFound = !repository || repository.includes('${') || repositories.includes(repository);
267
+
268
+ if (!repositoryFound) {
269
+ repository = '';
270
+ }
271
+
272
+ const { tag, tags } = this.getTags(props.tag, this.repositoryMap, repository);
273
+ const tagFound = tag === props.tag || specifyTagByRegex;
274
+
275
+ const newState = {
276
+ accountOptions: this.newAccounts.sort().map((a) => ({ label: a, value: a })),
277
+ organizationOptions: this.organizations
278
+ .filter((o) => o)
279
+ .sort()
280
+ .map((o) => ({ label: o, value: o })),
281
+ imagesLoaded: true,
282
+ repositoryOptions: repositories.sort().map((r) => ({ label: r, value: r })),
283
+ tagOptions: tags.sort().map((t) => ({ label: t, value: t })),
284
+ } as IDockerChartAndTagSelectorState;
285
+
286
+ if (
287
+ imageId &&
288
+ (!this.state.imagesLoaded || allowAutoSwitchToManualEntry) &&
289
+ (!organizationFound || !repositoryFound || !tagFound)
290
+ ) {
291
+ newState.defineManually = true;
292
+
293
+ const missingFields: string[] = [];
294
+ if (!organizationFound) {
295
+ missingFields.push('organization');
296
+ }
297
+ if (!repositoryFound) {
298
+ missingFields.push('image');
299
+ }
300
+ if (!tagFound) {
301
+ missingFields.push('tag');
302
+ }
303
+ newState.missingFields = missingFields;
304
+ newState.switchedManualWarning = `Could not find ${missingFields.join(' or ')}, switched to manual entry`;
305
+ } else if (!imageId || !imageId.includes('${')) {
306
+ this.synchronizeChanges(
307
+ this.state.defineManually
308
+ ? DockerImageUtils.splitImageId(imageId)
309
+ : { organization, repository, tag, digest: this.props.digest },
310
+ registry,
311
+ );
312
+ }
313
+
314
+ this.setState(newState);
315
+ }
316
+
317
+ private initializeImages(props: IDockerChartAndTagSelectorProps) {
318
+ if (this.state.imagesLoading) {
319
+ return;
320
+ }
321
+
322
+ const { showRegistry, account, registry } = props;
323
+
324
+ const imageConfig: IFindImageParams = {
325
+ provider: 'dockerRegistry',
326
+ account: showRegistry ? account : registry,
327
+ };
328
+
329
+ this.setState({ imagesLoading: true });
330
+ DockerChartImageReader.findImages(imageConfig)
331
+ .then((images: IDockerImage[]) => {
332
+ this.images = images;
333
+ this.registryMap = this.getRegistryMap(this.images);
334
+ this.accountMap = this.getAccountMap(this.images);
335
+ this.newAccounts = this.accounts || Object.keys(this.accountMap);
336
+
337
+ this.organizationMap = this.getOrganizationMap(this.images);
338
+ this.repositoryMap = this.getRepositoryMap(this.images);
339
+ this.organizations = this.getOrganizationsList(this.accountMap);
340
+ this.updateThings(props, true);
341
+ })
342
+ .finally(() => {
343
+ if (!this.unmounted) {
344
+ this.setState({ imagesLoading: false });
345
+ }
346
+ });
347
+ }
348
+
349
+ public handleRefreshImages = (): void => {
350
+ this.refreshImages(this.props);
351
+ };
352
+
353
+ public refreshImages(props: IDockerChartAndTagSelectorProps): void {
354
+ this.initializeImages(props);
355
+ }
356
+
357
+ private initializeAccounts(props: IDockerChartAndTagSelectorProps) {
358
+ let { account } = props;
359
+ AccountService.listAccounts('dockerRegistry').then((allAccounts: IAccount[]) => {
360
+ const accounts = allAccounts.map((a: IAccount) => a.name);
361
+ if (this.props.showRegistry && !account) {
362
+ account = accounts[0];
363
+ }
364
+ this.accounts = accounts;
365
+ this.refreshImages({ ...props, ...{ account } });
366
+ });
367
+ }
368
+
369
+ private isNew(): boolean {
370
+ const { account, organization, registry, repository, tag } = this.props;
371
+ return !account && !organization && !registry && !repository && !tag;
372
+ }
373
+
374
+ public componentDidMount() {
375
+ if (!this.props.deferInitialization && (this.props.registry || this.isNew())) {
376
+ this.initializeAccounts(this.props);
377
+ }
378
+ }
379
+
380
+ private valueChanged(name: string, value: string) {
381
+ const changes = { [name]: value };
382
+ if (imageFields.some((n) => n === name)) {
383
+ // values are parts of the image
384
+ const { organization, repository, tag, digest } = this.props;
385
+ const imageParts = { ...{ organization, repository, tag, digest }, ...changes };
386
+ const imageId = DockerImageUtils.generateImageId(imageParts);
387
+ changes.imageId = imageId;
388
+ }
389
+ this.props.onChange && this.props.onChange(changes);
390
+ }
391
+
392
+ private lookupTypeChanged = (o: Option<IDockerLookupType>) => {
393
+ const newType = o.value;
394
+ const oldType = this.state.lookupType;
395
+ const oldValue = this.props[oldType];
396
+ const cachedValue = this.cachedValues[newType];
397
+
398
+ this.valueChanged(oldType, undefined);
399
+ if (this.cachedValues[newType]) {
400
+ this.valueChanged(newType, cachedValue);
401
+ }
402
+ this.setState({ lookupType: newType });
403
+ this.cachedValues[oldType] = oldValue;
404
+ };
405
+
406
+ private showManualInput = (defineManually: boolean) => {
407
+ if (!defineManually) {
408
+ const newFields = DockerImageUtils.splitImageId(this.props.imageId || '');
409
+ this.props.onChange(newFields);
410
+ if (this.state.switchedManualWarning) {
411
+ this.setState({ switchedManualWarning: undefined, missingFields: undefined });
412
+ }
413
+ }
414
+ this.setState({ defineManually });
415
+ };
416
+
417
+ public render() {
418
+ const {
419
+ account,
420
+ allowManualDefinition,
421
+ digest,
422
+ imageId,
423
+ organization,
424
+ repository,
425
+ showDigest,
426
+ showRegistry,
427
+ specifyTagByRegex,
428
+ tag,
429
+ } = this.props;
430
+ const {
431
+ accountOptions,
432
+ switchedManualWarning,
433
+ missingFields,
434
+ imagesLoading,
435
+ lookupType,
436
+ organizationOptions,
437
+ repositoryOptions,
438
+ defineManually,
439
+ tagOptions,
440
+ } = this.state;
441
+
442
+ const parsedImageId = DockerImageUtils.splitImageId(imageId);
443
+
444
+ const manualInputToggle = (
445
+ <div className="sp-formItem groupHeader">
446
+ <div className="sp-formItem__left">
447
+ <div className="sp-formLabel">Define Image ID</div>
448
+
449
+ <div className="sp-formActions sp-formActions--mobile">
450
+ <span className="action" />
451
+ </div>
452
+ </div>
453
+
454
+ <div className="sp-formItem__right">
455
+ <div className="sp-form">
456
+ <span className="field">
457
+ <Select
458
+ value={defineManually}
459
+ disabled={imagesLoading || !allowManualDefinition}
460
+ onChange={(o: Option<boolean>) => this.showManualInput(o.value)}
461
+ options={defineOptions}
462
+ clearable={false}
463
+ />
464
+ </span>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ );
469
+
470
+ const warning = switchedManualWarning ? (
471
+ <div className="sp-formItem">
472
+ <div className="sp-formItem__left" />
473
+ <div className="sp-formItem__right">
474
+ <ValidationMessage
475
+ type="warning"
476
+ message={
477
+ <>
478
+ {switchedManualWarning}
479
+ {(missingFields || []).map((f) => (
480
+ <div key={f}>
481
+ <HelpField expand={true} id={`pipeline.config.docker.trigger.missing.${f}`} />
482
+ </div>
483
+ ))}
484
+ </>
485
+ }
486
+ />
487
+ </div>
488
+ </div>
489
+ ) : null;
490
+
491
+ if (defineManually) {
492
+ return (
493
+ <div className="sp-formGroup">
494
+ {manualInputToggle}
495
+ <div className="sp-formItem">
496
+ <div className="sp-formItem__left">
497
+ <div className="sp-formLabel">Image ID</div>
498
+ <div className="sp-formActions sp-formActions--mobile">
499
+ <span className="action" />
500
+ </div>
501
+ </div>
502
+ <div className="sp-formItem__right">
503
+ <div className="sp-form">
504
+ <span className="field">
505
+ <input
506
+ className="form-control input-sm"
507
+ value={imageId || ''}
508
+ onChange={(e) => this.valueChanged('imageId', e.target.value)}
509
+ />
510
+ </span>
511
+ </div>
512
+ </div>
513
+ </div>
514
+ {warning}
515
+ </div>
516
+ );
517
+ }
518
+
519
+ const Registry = showRegistry ? (
520
+ <div className="sp-formItem">
521
+ <div className="sp-formItem__left">
522
+ <div className="sp-formLabel">Registry Name</div>
523
+ </div>
524
+ <div className="sp-formItem__right">
525
+ <div className="sp-form">
526
+ <span className="field">
527
+ <Select
528
+ value={account}
529
+ disabled={imagesLoading}
530
+ onChange={(o: Option<string>) => this.valueChanged('account', o ? o.value : '')}
531
+ options={accountOptions}
532
+ isLoading={imagesLoading}
533
+ />
534
+ </span>
535
+ <span className="sp-formActions sp-formActions--web">
536
+ <span className="action">
537
+ <Tooltip value={imagesLoading ? 'Images refreshing' : 'Refresh images list'}>
538
+ <i
539
+ className={`fa icon-button-refresh-arrows ${imagesLoading ? 'fa-spin' : ''}`}
540
+ onClick={this.handleRefreshImages}
541
+ />
542
+ </Tooltip>
543
+ </span>
544
+ </span>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ ) : null;
549
+
550
+ const Organization = (
551
+ <div className="sp-formItem">
552
+ <div className="sp-formItem__left">
553
+ <div className="sp-formLabel">Organization</div>
554
+ </div>
555
+
556
+ <div className="sp-formItem__right">
557
+ <div className="sp-form">
558
+ <span className="field">
559
+ {organization.includes('${') ? (
560
+ <input
561
+ disabled={imagesLoading}
562
+ className="form-control input-sm"
563
+ value={organization || ''}
564
+ onChange={(e) => this.valueChanged('organization', e.target.value)}
565
+ />
566
+ ) : (
567
+ <Select
568
+ value={organization || ''}
569
+ disabled={imagesLoading}
570
+ onChange={(o: Option<string>) => this.valueChanged('organization', (o && o.value) || '')}
571
+ placeholder="No organization"
572
+ options={organizationOptions}
573
+ isLoading={imagesLoading}
574
+ />
575
+ )}
576
+ </span>
577
+ </div>
578
+ </div>
579
+ </div>
580
+ );
581
+
582
+ const Image = (
583
+ <div className="sp-formItem">
584
+ <div className="sp-formItem__left">
585
+ <div className="sp-formLabel">Image</div>
586
+ </div>
587
+
588
+ <div className="sp-formItem__right">
589
+ <div className="sp-form">
590
+ <span className="field">
591
+ {repository.includes('${') ? (
592
+ <input
593
+ className="form-control input-sm"
594
+ disabled={imagesLoading}
595
+ value={repository || ''}
596
+ onChange={(e) => this.valueChanged('repository', e.target.value)}
597
+ />
598
+ ) : (
599
+ <Select
600
+ value={repository || ''}
601
+ disabled={imagesLoading}
602
+ onChange={(o: Option<string>) => this.valueChanged('repository', (o && o.value) || '')}
603
+ options={repositoryOptions}
604
+ required={true}
605
+ isLoading={imagesLoading}
606
+ />
607
+ )}
608
+ </span>
609
+ </div>
610
+ </div>
611
+ </div>
612
+ );
613
+
614
+ const Tag =
615
+ lookupType === 'tag' ? (
616
+ specifyTagByRegex ? (
617
+ <div className="sp-formItem">
618
+ <div className="sp-formItem__left">
619
+ <div className="sp-formLabel">Tag</div>
620
+ </div>
621
+
622
+ <div className="sp-formItem__right">
623
+ <div className="sp-form">
624
+ <span className="field">
625
+ <input
626
+ type="text"
627
+ className="form-control input-sm"
628
+ value={tag || ''}
629
+ disabled={imagesLoading || !repository}
630
+ onChange={(e) => this.valueChanged('tag', e.target.value)}
631
+ />
632
+ </span>
633
+ </div>
634
+ <HelpField id="pipeline.config.docker.trigger.tag" expand={true} />
635
+ </div>
636
+ </div>
637
+ ) : (
638
+ <div className="sp-formItem">
639
+ <div className="sp-formItem__left">
640
+ <div className="sp-formLabel">Tag</div>
641
+ </div>
642
+
643
+ <div className="sp-formItem__right">
644
+ <div className="sp-form">
645
+ <span className="field">
646
+ {tag && tag.includes('${') ? (
647
+ <input
648
+ className="form-control input-sm"
649
+ disabled={imagesLoading}
650
+ value={tag || ''}
651
+ onChange={(e) => this.valueChanged('tag', e.target.value)}
652
+ required={true}
653
+ />
654
+ ) : (
655
+ <>
656
+ <Select
657
+ value={tag || ''}
658
+ disabled={imagesLoading || !repository}
659
+ isLoading={imagesLoading}
660
+ onChange={(o: Option<string>) => this.valueChanged('tag', o ? o.value : undefined)}
661
+ options={tagOptions}
662
+ placeholder="No tag"
663
+ required={true}
664
+ />
665
+ <HelpField id="pipeline.config.docker.trigger.tag.additionalInfo" expand={true} />
666
+ </>
667
+ )}
668
+ </span>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ )
673
+ ) : null;
674
+
675
+ const Digest =
676
+ lookupType === 'digest' ? (
677
+ <div className="sp-formItem">
678
+ <div className="sp-formItem__left">
679
+ <div className="sp-formLabel">
680
+ Digest <HelpField id="pipeline.config.docker.trigger.digest" />
681
+ </div>
682
+ </div>
683
+ <div className="sp-formItem__right">
684
+ <div className="sp-form">
685
+ <span className="field">
686
+ <input
687
+ className="form-control input-sm"
688
+ placeholder="sha256:abc123"
689
+ value={digest || parsedImageId.digest || ''}
690
+ onChange={(e) => this.valueChanged('digest', e.target.value)}
691
+ required={true}
692
+ />
693
+ </span>
694
+ </div>
695
+ </div>
696
+ </div>
697
+ ) : null;
698
+
699
+ const LookupTypeSelector = showDigest ? (
700
+ <div className="sp-formItem">
701
+ <div className="sp-formItem__left">
702
+ <div className="sp-formLabel">Type</div>
703
+ </div>
704
+
705
+ <div className="sp-formItem__right">
706
+ <div className="sp-form">
707
+ <span className="field">
708
+ <Select
709
+ clearable={false}
710
+ value={lookupType}
711
+ options={[
712
+ { value: 'digest', label: 'Digest' },
713
+ { value: 'tag', label: 'Tag' },
714
+ ]}
715
+ onChange={this.lookupTypeChanged}
716
+ />
717
+ </span>
718
+ </div>
719
+ </div>
720
+ </div>
721
+ ) : null;
722
+
723
+ return (
724
+ <div className="sp-formGroup">
725
+ {manualInputToggle}
726
+ {Registry}
727
+ {Organization}
728
+ {Image}
729
+ {LookupTypeSelector}
730
+ {Digest}
731
+ {Tag}
732
+ </div>
733
+ );
734
+ }
735
+ }