@orsetra/shared-ui 1.1.9 → 1.1.11

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.
Files changed (31) hide show
  1. package/components/extends/ArrayItemGroup/index.tsx +58 -0
  2. package/components/extends/CPUNumber/index.tsx +33 -0
  3. package/components/extends/ClassStorageSelect/index.tsx +11 -0
  4. package/components/extends/ClusterSelect/index.tsx +11 -0
  5. package/components/extends/ComponentSelect/index.tsx +114 -0
  6. package/components/extends/DiskNumber/index.tsx +34 -0
  7. package/components/extends/EnvSelect/index.tsx +11 -0
  8. package/components/extends/Group/index.tsx +152 -0
  9. package/components/extends/Ignore/index.tsx +11 -0
  10. package/components/extends/ImageInput/index.less +49 -0
  11. package/components/extends/ImageInput/index.tsx +195 -0
  12. package/components/extends/ImageSecretSelect/index.tsx +170 -0
  13. package/components/extends/KV/index.tsx +23 -0
  14. package/components/extends/MemoryNumber/index.tsx +34 -0
  15. package/components/extends/Numbers/index.tsx +23 -0
  16. package/components/extends/PVCSelect/index.tsx +12 -0
  17. package/components/extends/PolicySelect/index.tsx +55 -0
  18. package/components/extends/SecretKeySelect/index.tsx +97 -0
  19. package/components/extends/SecretSelect/index.tsx +78 -0
  20. package/components/extends/StepSelect/index.tsx +76 -0
  21. package/components/extends/Strings/index.tsx +23 -0
  22. package/components/extends/Switch/index.tsx +23 -0
  23. package/components/extends/index.ts +21 -0
  24. package/components/ui/combobox.tsx +137 -0
  25. package/components/ui/index.ts +3 -0
  26. package/components/ui/kv-input.tsx +117 -0
  27. package/components/ui/multi-select.tsx +151 -0
  28. package/components/ui/numbers-input.tsx +87 -0
  29. package/components/ui/strings-input.tsx +83 -0
  30. package/index.ts +12 -0
  31. package/package.json +2 -2
@@ -0,0 +1,170 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/ui/command';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { cn } from '@/lib/utils';
10
+ import i18n from '@/i18n';
11
+ import type { GetImageReposResponse, ImageRegistry } from '@/api/repository';
12
+
13
+
14
+ type Props = {
15
+ value?: string[];
16
+ onChange: (value: string[]) => void;
17
+ id: string;
18
+ disabled: boolean;
19
+ project?: string;
20
+ getImageRepos: (params: { project: string }) => Promise<GetImageReposResponse>;
21
+ };
22
+
23
+ const ImageSecretSelect: React.FC<Props> = ({
24
+ value = [],
25
+ id,
26
+ onChange,
27
+ disabled,
28
+ project,
29
+ getImageRepos,
30
+ }) => {
31
+ const [open, setOpen] = useState(false);
32
+ const [loading, setLoading] = useState(false);
33
+ const [registries, setRegistries] = useState<ImageRegistry[]>([]);
34
+ const [searchValue, setSearchValue] = useState('');
35
+
36
+ useEffect(() => {
37
+ if (project) {
38
+ setLoading(true);
39
+ getImageRepos({ project })
40
+ .then((res) => {
41
+ if (res) {
42
+ setRegistries(res.registries || []);
43
+ }
44
+ })
45
+ .finally(() => {
46
+ setLoading(false);
47
+ });
48
+ }
49
+ }, [project]);
50
+
51
+ const convertImageRegistryOptions = (data: ImageRegistry[]): Array<{ label: string; value: string }> => {
52
+ return (data || []).map((item: ImageRegistry) => {
53
+ let label = item.secretName;
54
+ if (item.domain) {
55
+ label = `${item.secretName} (${item.domain})`;
56
+ }
57
+ return { label, value: item.secretName };
58
+ });
59
+ };
60
+
61
+ const handleSelect = (selectedValue: string) => {
62
+ const newValue = value.includes(selectedValue)
63
+ ? value.filter((v) => v !== selectedValue)
64
+ : [...value, selectedValue];
65
+ onChange(newValue);
66
+ };
67
+
68
+ const handleRemove = (valueToRemove: string) => {
69
+ onChange(value.filter((v) => v !== valueToRemove));
70
+ };
71
+
72
+ const dataSource = [...registries];
73
+ if (searchValue && !dataSource.some(r => r.secretName === searchValue)) {
74
+ dataSource.unshift({ secretName: searchValue, name: searchValue });
75
+ }
76
+
77
+ const options = convertImageRegistryOptions(dataSource);
78
+ const selectedLabels = options
79
+ .filter((opt) => value.includes(opt.value))
80
+ .map((opt) => opt.label);
81
+
82
+ return (
83
+ <div className="w-full">
84
+ <Popover open={open} onOpenChange={setOpen}>
85
+ <PopoverTrigger asChild>
86
+ <Button
87
+ id={id}
88
+ variant="secondary"
89
+ role="combobox"
90
+ aria-expanded={open}
91
+ disabled={disabled || loading}
92
+ className="w-full justify-between h-auto min-h-[40px] px-3 py-2"
93
+ style={{ borderRadius: 0 }}
94
+ >
95
+ <div className="flex flex-wrap gap-1 flex-1">
96
+ {loading ? (
97
+ <div className="flex items-center gap-2">
98
+ <Loader2 className="h-4 w-4 animate-spin" />
99
+ <span className="text-ibm-gray-50">Loading...</span>
100
+ </div>
101
+ ) : value.length === 0 ? (
102
+ <span className="text-ibm-gray-50">
103
+ {i18n.t('Please select or input your owner image registry secret')}
104
+ </span>
105
+ ) : (
106
+ selectedLabels.map((label, index) => (
107
+ <Badge
108
+ key={value[index]}
109
+ variant="secondary"
110
+ className="mr-1"
111
+ style={{ borderRadius: 0 }}
112
+ >
113
+ {label}
114
+ <button
115
+ className="ml-1 hover:text-ibm-red-60"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ handleRemove(value[index]);
119
+ }}
120
+ >
121
+ <X className="h-3 w-3" />
122
+ </button>
123
+ </Badge>
124
+ ))
125
+ )}
126
+ </div>
127
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
128
+ </Button>
129
+ </PopoverTrigger>
130
+ <PopoverContent className="w-full p-0" style={{ borderRadius: 0 }}>
131
+ <Command>
132
+ <CommandInput
133
+ placeholder="Search or enter custom secret name..."
134
+ value={searchValue}
135
+ onValueChange={setSearchValue}
136
+ />
137
+ <CommandEmpty>
138
+ {searchValue ? (
139
+ <div className="p-2 text-sm">
140
+ Press Enter to add "{searchValue}"
141
+ </div>
142
+ ) : (
143
+ 'No registry found.'
144
+ )}
145
+ </CommandEmpty>
146
+ <CommandGroup className="max-h-64 overflow-auto">
147
+ {options.map((option) => (
148
+ <CommandItem
149
+ key={option.value}
150
+ value={option.value}
151
+ onSelect={() => handleSelect(option.value)}
152
+ >
153
+ <Check
154
+ className={cn(
155
+ "mr-2 h-4 w-4",
156
+ value.includes(option.value) ? "opacity-100" : "opacity-0"
157
+ )}
158
+ />
159
+ {option.label}
160
+ </CommandItem>
161
+ ))}
162
+ </CommandGroup>
163
+ </Command>
164
+ </PopoverContent>
165
+ </Popover>
166
+ </div>
167
+ );
168
+ };
169
+
170
+ export default ImageSecretSelect;
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { KVInput } from '../../ui/kv-input';
3
+
4
+ type Props = {
5
+ value?: Record<string, string>;
6
+ onChange?: (value: Record<string, string>) => void;
7
+ id: string;
8
+ disabled: boolean;
9
+ };
10
+
11
+ const KV: React.FC<Props> = ({ value, onChange, disabled }) => {
12
+ return (
13
+ <KVInput
14
+ value={value}
15
+ onChange={onChange}
16
+ disabled={disabled}
17
+ keyPlaceholder="Key"
18
+ valuePlaceholder="Value"
19
+ />
20
+ );
21
+ };
22
+
23
+ export default KV;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Input } from '../../ui/input';
3
+
4
+ type Props = {
5
+ id: string;
6
+ onChange: (value: any) => void;
7
+ value?: any;
8
+ disabled: boolean;
9
+ };
10
+
11
+ const MemoryNumber: React.FC<Props> = ({ value, id, onChange, disabled }) => {
12
+ const initValue = value ? parseInt(value.replace('Mi', ''), 10) : undefined;
13
+
14
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
15
+ onChange(e.target.value + 'Mi');
16
+ };
17
+
18
+ return (
19
+ <div className="flex items-center gap-2">
20
+ <Input
21
+ id={id}
22
+ type="number"
23
+ min="0"
24
+ disabled={disabled}
25
+ onChange={handleChange}
26
+ value={initValue}
27
+ className="flex-1"
28
+ />
29
+ <span className="text-sm text-ibm-gray-70">Mi</span>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default MemoryNumber;
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { NumbersInput } from '@/components/ui/numbers-input';
3
+
4
+ type Props = {
5
+ label?: string;
6
+ value?: number[];
7
+ id: string;
8
+ onChange: (value: number[]) => void;
9
+ disabled: boolean;
10
+ };
11
+
12
+ const Numbers: React.FC<Props> = ({ value, onChange, disabled }) => {
13
+ return (
14
+ <NumbersInput
15
+ value={value}
16
+ onChange={onChange}
17
+ disabled={disabled}
18
+ placeholder="Enter number"
19
+ />
20
+ );
21
+ };
22
+
23
+ export default Numbers;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ type Props = {
4
+ clusterName: string;
5
+ namespace: string;
6
+ };
7
+
8
+ const PVCSelect: React.FC<Props> = ({ clusterName, namespace }) => {
9
+ return <div />;
10
+ };
11
+
12
+ export default PVCSelect;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { MultiSelect } from '../../ui/multi-select';
5
+ import type { ApplicationPolicyBase } from '@/api';
6
+
7
+ type Props = {
8
+ onChange: (value: any) => void;
9
+ value?: any;
10
+ id: string;
11
+ disabled: boolean;
12
+ appName: string;
13
+ getPolicyList: (params: { appName: string }) => Promise<{ policies: ApplicationPolicyBase[] }>;
14
+ };
15
+
16
+ const PolicySelect: React.FC<Props> = ({ value, id, disabled, onChange, appName, getPolicyList }) => {
17
+ const [policySelectDataSource, setPolicySelectDataSource] = useState<Array<{ label: string; value: string }>>([]);
18
+
19
+ useEffect(() => {
20
+ const fetchPolicyList = async () => {
21
+ if (appName) {
22
+ try {
23
+ const res = await getPolicyList({ appName });
24
+ if (res && res.policies) {
25
+ const policyListData = (res.policies || []).map((item: ApplicationPolicyBase) => ({
26
+ label: `${item.name}(${item.type})`,
27
+ value: item.name,
28
+ }));
29
+ setPolicySelectDataSource(policyListData);
30
+ } else {
31
+ setPolicySelectDataSource([]);
32
+ }
33
+ } catch {
34
+ setPolicySelectDataSource([]);
35
+ }
36
+ }
37
+ };
38
+
39
+ fetchPolicyList();
40
+ }, [appName, getPolicyList]);
41
+
42
+ return (
43
+ <MultiSelect
44
+ placeholder="Please select"
45
+ onChange={onChange}
46
+ id={id}
47
+ disabled={disabled}
48
+ defaultValue={value || []}
49
+ value={value || []}
50
+ dataSource={policySelectDataSource}
51
+ />
52
+ );
53
+ };
54
+
55
+ export default PolicySelect;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import React, { useState } from 'react';
4
+ import { Check, ChevronsUpDown } from 'lucide-react';
5
+ import { Button } from '../../ui/button';
6
+ import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../../ui/command';
8
+ import { cn } from '../../../lib/utils';
9
+ import i18n from '@/i18n';
10
+
11
+ type Props = {
12
+ onChange: (value: any) => void;
13
+ secretKeys?: string[];
14
+ value?: any;
15
+ id: string;
16
+ disabled: boolean;
17
+ };
18
+
19
+ const SecretKeySelect: React.FC<Props> = ({ onChange, value, secretKeys, id, disabled }) => {
20
+ const [open, setOpen] = useState(false);
21
+ const [searchValue, setSearchValue] = useState('');
22
+
23
+ const dataSource = [...(secretKeys || [])];
24
+ if (searchValue && !dataSource.includes(searchValue)) {
25
+ dataSource.unshift(searchValue);
26
+ }
27
+
28
+ const options = dataSource.map((item) => ({
29
+ label: item,
30
+ value: item,
31
+ }));
32
+
33
+ const handleSelect = (selectedValue: string) => {
34
+ onChange(selectedValue);
35
+ setOpen(false);
36
+ };
37
+
38
+ const selectedLabel = options.find((opt) => opt.value === value)?.label || value;
39
+
40
+ return (
41
+ <Popover open={open} onOpenChange={setOpen}>
42
+ <PopoverTrigger asChild>
43
+ <Button
44
+ id={id}
45
+ variant="secondary"
46
+ role="combobox"
47
+ aria-expanded={open}
48
+ disabled={disabled}
49
+ className="w-full justify-between h-[40px] px-3"
50
+ style={{ borderRadius: 0 }}
51
+ >
52
+ <span className={cn(!value && "text-ibm-gray-50")}>
53
+ {value ? selectedLabel : i18n.t('Please select or input a secret key')}
54
+ </span>
55
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
56
+ </Button>
57
+ </PopoverTrigger>
58
+ <PopoverContent className="w-full p-0" style={{ borderRadius: 0 }}>
59
+ <Command>
60
+ <CommandInput
61
+ placeholder="Search or enter custom secret key..."
62
+ value={searchValue}
63
+ onValueChange={setSearchValue}
64
+ />
65
+ <CommandEmpty>
66
+ {searchValue ? (
67
+ <div className="p-2 text-sm">
68
+ Press Enter to add "{searchValue}"
69
+ </div>
70
+ ) : (
71
+ 'No secret key found.'
72
+ )}
73
+ </CommandEmpty>
74
+ <CommandGroup className="max-h-64 overflow-auto">
75
+ {options.map((option) => (
76
+ <CommandItem
77
+ key={option.value}
78
+ value={option.value}
79
+ onSelect={() => handleSelect(option.value)}
80
+ >
81
+ <Check
82
+ className={cn(
83
+ "mr-2 h-4 w-4",
84
+ value === option.value ? "opacity-100" : "opacity-0"
85
+ )}
86
+ />
87
+ {option.label}
88
+ </CommandItem>
89
+ ))}
90
+ </CommandGroup>
91
+ </Command>
92
+ </PopoverContent>
93
+ </Popover>
94
+ );
95
+ };
96
+
97
+ export default SecretKeySelect;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react';
4
+ import { Combobox } from '../../ui/combobox';
5
+
6
+ import { locale } from '@/utils/locale';
7
+ import type { Secret } from '@/api';
8
+
9
+ type Props = {
10
+ onChange: (value: any) => void;
11
+ setKeys: (keys: string[]) => void;
12
+ value?: any;
13
+ id: string;
14
+ appNamespace?: string;
15
+ disabled: boolean;
16
+ listCloudResourceSecrets: (params: { appNs: string }) => Promise<{ secrets: Secret[] }>;
17
+ };
18
+
19
+ const SecretSelect: React.FC<Props> = ({ value, id, disabled, appNamespace, onChange, setKeys, listCloudResourceSecrets }) => {
20
+ const [secrets, setSecrets] = useState<Secret[]>([]);
21
+
22
+ const getSecretKeys = useCallback((name: string) => {
23
+ let keys: string[] = [];
24
+ secrets?.forEach((secret) => {
25
+ if (secret.metadata.labels['app.oam.dev/sync-alias'] === name && 'data' in secret) {
26
+ keys = Object.keys(secret.data);
27
+ }
28
+ });
29
+ return keys;
30
+ }, [secrets]);
31
+
32
+ useEffect(() => {
33
+ const loadSecrets = async () => {
34
+ if (appNamespace) {
35
+ try {
36
+ const res = await listCloudResourceSecrets({ appNs: appNamespace });
37
+ if (res) {
38
+ setSecrets(res.secrets);
39
+ const keys = getSecretKeys(value);
40
+ setKeys(keys);
41
+ }
42
+ } catch {
43
+ setSecrets([]);
44
+ }
45
+ }
46
+ };
47
+
48
+ loadSecrets();
49
+ }, [appNamespace, value, setKeys, getSecretKeys, listCloudResourceSecrets]);
50
+
51
+ const handleChange = (selectedValue: string) => {
52
+ const keys = getSecretKeys(selectedValue);
53
+ onChange(selectedValue);
54
+ setKeys(keys);
55
+ };
56
+
57
+ const filters = secrets?.filter((secret) => secret.metadata.labels['app.oam.dev/sync-alias']);
58
+ const dataSource =
59
+ filters?.map((secret) => ({
60
+ label: secret.metadata.labels['app.oam.dev/sync-alias'],
61
+ value: secret.metadata.labels['app.oam.dev/sync-alias'],
62
+ })) || [];
63
+
64
+ return (
65
+ <Combobox
66
+ locale={locale().Select}
67
+ onChange={handleChange}
68
+ value={value}
69
+ id={id}
70
+ disabled={disabled}
71
+ placeholder="Please select or input a secret name"
72
+ enableInput={true}
73
+ dataSource={dataSource}
74
+ />
75
+ );
76
+ };
77
+
78
+ export default SecretSelect;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import React, { useContext } from 'react';
4
+
5
+ import { MultiSelect } from '@/components/ui/multi-select';
6
+ import { WorkflowContext, WorkflowEditContext } from '@/context';
7
+ import i18n from '@/i18n';
8
+ import type { WorkflowStep, WorkflowStepBase } from '@/api';
9
+ import { showAlias } from '@/utils/common';
10
+ import { locale } from '@/utils/locale';
11
+
12
+ type Props = {
13
+ value?: string[];
14
+ id: string;
15
+ onChange: (value: string[]) => void;
16
+ disabled?: boolean;
17
+ };
18
+
19
+ export const StepSelect = (props: Props) => {
20
+ const { value, id, disabled } = props;
21
+ const { stepName, steps } = useContext(WorkflowEditContext);
22
+ const { workflow } = useContext(WorkflowContext);
23
+ const stepOptions: Array<{ label: string; value: string }> = [];
24
+ let inGroup = false;
25
+ let groupStep: WorkflowStep | undefined;
26
+ steps?.map((step) => {
27
+ step.subSteps?.map((subStep) => {
28
+ if (subStep.name === stepName) {
29
+ inGroup = true;
30
+ groupStep = step;
31
+ }
32
+ });
33
+ });
34
+ if (workflow?.mode === 'DAG' && (workflow.subMode === 'DAG' || (workflow.subMode === 'StepByStep' && !inGroup))) {
35
+ steps
36
+ ?.filter((s) => s.name !== stepName)
37
+ .map((step: WorkflowStep) => {
38
+ stepOptions.push({
39
+ label: showAlias(step.name, step.alias),
40
+ value: step.name,
41
+ });
42
+ step.subSteps
43
+ ?.filter((s) => s.name !== stepName)
44
+ .map((b: WorkflowStepBase) => {
45
+ stepOptions.push({
46
+ label: `${showAlias(step.name, step.alias)}/${showAlias(b.name, b.alias)}`,
47
+ value: b.name,
48
+ });
49
+ });
50
+ });
51
+ }
52
+
53
+ if (workflow?.mode === 'StepByStep' && workflow.subMode === 'DAG' && inGroup && groupStep) {
54
+ groupStep.subSteps
55
+ ?.filter((s) => s.name !== stepName)
56
+ .map((step: WorkflowStep) => {
57
+ stepOptions.push({
58
+ label: showAlias(step.name, step.alias),
59
+ value: step.name,
60
+ });
61
+ });
62
+ }
63
+
64
+ return (
65
+ <MultiSelect
66
+ placeholder={i18n.t('Please select the steps')}
67
+ onChange={props.onChange}
68
+ id={id}
69
+ disabled={disabled}
70
+ defaultValue={value || []}
71
+ value={value || []}
72
+ dataSource={stepOptions}
73
+ locale={locale().Select}
74
+ />
75
+ );
76
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { StringsInput } from '@/components/ui/strings-input';
3
+
4
+ type Props = {
5
+ label?: string;
6
+ value?: string[];
7
+ id: string;
8
+ onChange: (value: string[]) => void;
9
+ disabled: boolean;
10
+ };
11
+
12
+ const Strings: React.FC<Props> = ({ value, onChange, disabled }) => {
13
+ return (
14
+ <StringsInput
15
+ value={value}
16
+ onChange={onChange}
17
+ disabled={disabled}
18
+ placeholder="Enter value"
19
+ />
20
+ );
21
+ };
22
+
23
+ export default Strings;
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { Switch } from '@/components/ui/switch';
3
+
4
+ type Props = {
5
+ value: boolean;
6
+ id: string;
7
+ onChange: (value: any) => void;
8
+ };
9
+
10
+ const SwitchComponent: React.FC<Props> = ({ value, id, onChange }) => {
11
+ return (
12
+ <div className="flex items-center gap-2">
13
+ <Switch
14
+ id={id}
15
+ checked={value}
16
+ onCheckedChange={onChange}
17
+ />
18
+ <span className="text-xs text-ibm-gray-70">{value ? 'on' : 'off'}</span>
19
+ </div>
20
+ );
21
+ };
22
+
23
+ export default SwitchComponent;
@@ -0,0 +1,21 @@
1
+ // Extended UI Components for Dynamic Forms
2
+ export { default as CPUNumber } from './CPUNumber'
3
+ export { default as ClassStorageSelect } from './ClassStorageSelect'
4
+ export { default as ClusterSelect } from './ClusterSelect'
5
+ export { default as ComponentSelect } from './ComponentSelect'
6
+ export { default as DiskNumber } from './DiskNumber'
7
+ export { default as EnvSelect } from './EnvSelect'
8
+ export { default as Group } from './Group'
9
+ export { default as Ignore } from './Ignore'
10
+ export { default as ImageInput } from './ImageInput'
11
+ export { default as ImageSecretSelect } from './ImageSecretSelect'
12
+ export { default as KV } from './KV'
13
+ export { default as MemoryNumber } from './MemoryNumber'
14
+ export { default as Numbers } from './Numbers'
15
+ export { default as PVCSelect } from './PVCSelect'
16
+ export { default as PolicySelect } from './PolicySelect'
17
+ export { default as SecretKeySelect } from './SecretKeySelect'
18
+ export { default as SecretSelect } from './SecretSelect'
19
+ export { StepSelect } from './StepSelect'
20
+ export { default as Strings } from './Strings'
21
+ export { default as FormSwitch } from './Switch'