Skip to content

Commit e6ccfb1

Browse files
committed
feat: Display secret values for auto-generate passwords in ServiceInputsSection
1 parent 0aa641b commit e6ccfb1

File tree

5 files changed

+143
-85
lines changed

5 files changed

+143
-85
lines changed

src/components/create-job/steps/deployment/ServiceDeployment.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ function ServiceDeployment({ isEditingJob }: { isEditingJob?: boolean }) {
5656
<AppParametersSection enableTunnelingLabel />
5757
</SlateCard>
5858

59-
{containerOrWorkerType.inputs && (
60-
<ServiceInputsSection inputs={containerOrWorkerType.inputs} isEditingJob={isEditingJob} />
61-
)}
59+
{containerOrWorkerType.inputs && <ServiceInputsSection inputs={containerOrWorkerType.inputs} />}
6260

6361
{/* <SlateCard title="Other">
6462
<InputWithLabel name="deployment.serviceReplica" label="Service Replica" placeholder="0x_ai" isOptional />

src/shared/InputWithLabel.tsx

Lines changed: 96 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,133 @@
11
import { InputProps } from '@heroui/input';
22
import StyledInput from '@shared/StyledInput';
3+
import { useState } from 'react';
34
import { Controller, useFormContext } from 'react-hook-form';
4-
import { RiClipboardLine } from 'react-icons/ri';
5+
import { RiCheckLine, RiClipboardLine, RiFileCopyLine } from 'react-icons/ri';
56
import Label from './Label';
7+
import SecretValueToggle from './jobs/SecretValueToggle';
68

79
interface Props extends InputProps {
810
name: string;
911
label: string;
1012
placeholder: string;
1113
isOptional?: boolean;
12-
displayPasteIcon?: boolean;
1314
onBlur?: () => void;
1415
onPasteValue?: (value: string) => void;
1516
customLabel?: React.ReactNode;
17+
endContent?: 'copy' | 'paste';
18+
hasSecretValue?: boolean;
1619
}
1720

1821
export default function InputWithLabel({
1922
name,
2023
label,
2124
placeholder,
2225
isOptional,
23-
displayPasteIcon,
2426
customLabel,
27+
endContent,
28+
hasSecretValue,
2529
...props
2630
}: Props) {
2731
const { control } = useFormContext();
2832

33+
const [copied, setCopied] = useState(false);
34+
const [isFieldSecret, setFieldSecret] = useState(!!hasSecretValue);
35+
2936
return (
3037
<div className="col w-full gap-2">
3138
{customLabel ? customLabel : <Label value={label} isOptional={isOptional} />}
3239

33-
<Controller
34-
name={name}
35-
control={control}
36-
render={({ field, fieldState }) => {
37-
if (name === 'deployment.deploymentType.crUsername') {
38-
console.log({ field, fieldState });
39-
}
40+
<div className="flex gap-3">
41+
{hasSecretValue && (
42+
<SecretValueToggle
43+
isSecret={isFieldSecret}
44+
onClick={() => {
45+
setFieldSecret((previous) => !previous);
46+
}}
47+
/>
48+
)}
4049

41-
return (
42-
<StyledInput
43-
placeholder={placeholder}
44-
value={field.value ?? ''}
45-
onChange={(e) => {
46-
const value = e.target.value;
47-
field.onChange(value);
48-
}}
49-
onBlur={() => {
50-
field.onBlur();
50+
<Controller
51+
name={name}
52+
control={control}
53+
render={({ field, fieldState }) => {
54+
if (name === 'deployment.deploymentType.crUsername') {
55+
console.log({ field, fieldState });
56+
}
5157

52-
if (props.onBlur) {
53-
props.onBlur();
54-
}
55-
}}
56-
onPaste={(e) => {
57-
const pastedText = e.clipboardData?.getData('text') ?? '';
58+
return (
59+
<StyledInput
60+
placeholder={placeholder}
61+
value={field.value ?? ''}
62+
onChange={(e) => {
63+
const value = e.target.value;
64+
field.onChange(value);
65+
}}
66+
onBlur={() => {
67+
field.onBlur();
5868

59-
if (props.onPasteValue) {
60-
props.onPasteValue(pastedText);
61-
}
62-
}}
63-
isInvalid={!!fieldState.error}
64-
errorMessage={fieldState.error?.message}
65-
endContent={
66-
displayPasteIcon ? (
67-
<div
68-
className="cursor-pointer hover:opacity-60"
69-
onClick={async () => {
70-
try {
71-
const clipboardText = await navigator.clipboard.readText();
72-
field.onChange(clipboardText);
69+
if (props.onBlur) {
70+
props.onBlur();
71+
}
72+
}}
73+
onPaste={(e) => {
74+
const pastedText = e.clipboardData?.getData('text') ?? '';
75+
76+
if (props.onPasteValue) {
77+
props.onPasteValue(pastedText);
78+
}
79+
}}
80+
isInvalid={!!fieldState.error}
81+
errorMessage={fieldState.error?.message}
82+
endContent={
83+
endContent === 'paste' ? (
84+
<div
85+
className="cursor-pointer hover:opacity-60"
86+
onClick={async () => {
87+
try {
88+
const clipboardText = await navigator.clipboard.readText();
89+
field.onChange(clipboardText);
7390

74-
if (props.onPasteValue) {
75-
props.onPasteValue(clipboardText);
91+
if (props.onPasteValue) {
92+
props.onPasteValue(clipboardText);
93+
}
94+
} catch (error) {
95+
console.error('Failed to read clipboard:', error);
7696
}
77-
} catch (error) {
78-
console.error('Failed to read clipboard:', error);
79-
}
80-
}}
81-
>
82-
<RiClipboardLine className="text-lg text-slate-600" />
83-
</div>
84-
) : undefined
85-
}
86-
{...props}
87-
/>
88-
);
89-
}}
90-
/>
97+
}}
98+
>
99+
<RiClipboardLine className="text-lg text-slate-600" />
100+
</div>
101+
) : endContent === 'copy' ? (
102+
<div
103+
className="cursor-pointer text-lg text-slate-600 hover:opacity-60"
104+
onClick={async () => {
105+
try {
106+
if (copied) {
107+
return;
108+
}
109+
110+
navigator.clipboard.writeText(field.value);
111+
setCopied(true);
112+
setTimeout(() => {
113+
setCopied(false);
114+
}, 1000);
115+
} catch (error) {
116+
console.error('Failed to copy to clipboard:', error);
117+
}
118+
}}
119+
>
120+
{copied ? <RiCheckLine /> : <RiFileCopyLine />}
121+
</div>
122+
) : undefined
123+
}
124+
type={isFieldSecret ? 'password' : 'text'}
125+
{...props}
126+
/>
127+
);
128+
}}
129+
/>
130+
</div>
91131
</div>
92132
);
93133
}

src/shared/jobs/DeeployInfo.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ReactNode } from 'react';
2+
import { RiInformationLine } from 'react-icons/ri';
3+
4+
export default function DeeployInfo({ title, description }: { title: ReactNode; description: ReactNode }) {
5+
return (
6+
<div className="col gap-2 rounded-md bg-blue-100 p-3 text-sm text-blue-600">
7+
<div className="row gap-1.5">
8+
<RiInformationLine className="text-[20px]" />
9+
10+
{title}
11+
</div>
12+
13+
<div>{description}</div>
14+
</div>
15+
);
16+
}

src/shared/jobs/ServiceInputsSection.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1+
import { generateSecurePassword, isKeySecret } from '@lib/utils';
12
import { SlateCard } from '@shared/cards/SlateCard';
23
import InputWithLabel from '@shared/InputWithLabel';
34
import { KeyValueEntryWithId } from '@typedefs/deeploys';
4-
import { useEffect } from 'react';
5+
import { useEffect, useState } from 'react';
56
import { useFieldArray, useFormContext } from 'react-hook-form';
7+
import DeeployInfo from './DeeployInfo';
68

7-
export default function ServiceInputsSection({
8-
inputs,
9-
isEditingJob,
10-
}: {
11-
inputs: { key: string; label: string }[];
12-
isEditingJob?: boolean;
13-
}) {
9+
export default function ServiceInputsSection({ inputs }: { inputs: { key: string; label: string }[] }) {
1410
const { control, setValue } = useFormContext();
1511

1612
const { fields } = useFieldArray({
@@ -20,21 +16,27 @@ export default function ServiceInputsSection({
2016

2117
const typedFields = fields as KeyValueEntryWithId[];
2218

19+
const [attemptedAutoGeneration, setAttemptedAutoGeneration] = useState(false);
20+
2321
useEffect(() => {
24-
setValue(
25-
'deployment.inputs',
26-
inputs.map((input) => {
27-
// let value = '';
22+
// If a job/job draft is not being edited and we haven't attempted to auto-generate passwords yet
23+
if (!fields.length && !attemptedAutoGeneration) {
24+
setValue(
25+
'deployment.inputs',
26+
inputs.map((input) => {
27+
let value = '';
28+
29+
if (isKeySecret(input.key)) {
30+
value = generateSecurePassword();
31+
}
2832

29-
if (!isEditingJob && input.key.toLowerCase().includes('password')) {
30-
// TODO: Only generate it if no default value (editing flow) exists
31-
// value = generateSecurePassword();
32-
}
33+
return { key: input.key, value };
34+
}),
35+
);
3336

34-
return { key: input.key, value: '' };
35-
}),
36-
);
37-
}, [inputs]);
37+
setAttemptedAutoGeneration(true);
38+
}
39+
}, [inputs, fields]);
3840

3941
return (
4042
<SlateCard title="Service Inputs">
@@ -46,15 +48,17 @@ export default function ServiceInputsSection({
4648
name={`deployment.inputs.${index}.value`}
4749
label={inputs[index].label}
4850
placeholder="Required"
51+
endContent={isKeySecret(field.key) ? 'copy' : undefined}
52+
hasSecretValue={isKeySecret(field.key)}
4953
/>
5054
</div>
5155
))}
5256
</div>
5357

54-
{/* TODO: Display after the input becomes dirty */}
55-
{inputs.some((input) => input.key.toLowerCase().includes('password')) && (
56-
<div className="text-sm text-slate-500">Don't forget to save your auto-generated password.</div>
57-
)}
58+
<DeeployInfo
59+
title="Auto-generated Passwords"
60+
description="Secure passwords were automatically generated for the required fields; don't forget to copy and save them."
61+
/>
5862
</div>
5963
</SlateCard>
6064
);

src/shared/jobs/deployment-type/WorkerSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default function WorkerSection({
9898
</div>
9999
) : null
100100
}
101-
displayPasteIcon
101+
endContent="paste"
102102
/>
103103

104104
<InputWithLabel name={`${baseName}.deploymentType.image`} label="Image" placeholder="node:22" />
@@ -130,7 +130,7 @@ export default function WorkerSection({
130130
label="Personal Access Token"
131131
placeholder="None"
132132
isOptional={repositoryVisibility === 'public'}
133-
displayPasteIcon
133+
endContent="paste"
134134
/>
135135
</div>
136136

0 commit comments

Comments
 (0)