mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-05-08 11:20:17 -05:00
Feat: improved mc secrets (#1585)
* make encryption service public * generate * generate * with update * revert * fix: type
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from './textarea';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TrashIcon } from '@radix-ui/react-icons';
|
||||
@@ -11,6 +11,9 @@ export type KeyValueType = {
|
||||
hidden: boolean;
|
||||
locked: boolean;
|
||||
deleted: boolean;
|
||||
id?: string; // Optional ID for existing env vars
|
||||
isEditing?: boolean; // Whether the value is being edited
|
||||
hint?: string; // Hint value for existing secrets
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
@@ -20,6 +23,9 @@ type PropsType = {
|
||||
disabled?: boolean;
|
||||
fileUpload?: boolean;
|
||||
secretOption?: boolean;
|
||||
onDeleteIds?: (ids: string[]) => void; // Callback for deleted IDs
|
||||
onAdd?: (values: KeyValueType[]) => void; // Callback for added values
|
||||
onUpdate?: (values: KeyValueType[]) => void; // Callback for updated values
|
||||
};
|
||||
|
||||
const EnvGroupArray: React.FC<PropsType> = ({
|
||||
@@ -28,6 +34,9 @@ const EnvGroupArray: React.FC<PropsType> = ({
|
||||
setValues = () => {},
|
||||
disabled,
|
||||
secretOption,
|
||||
onDeleteIds,
|
||||
onAdd,
|
||||
onUpdate,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!values) {
|
||||
@@ -37,102 +46,164 @@ const EnvGroupArray: React.FC<PropsType> = ({
|
||||
|
||||
const handleValueChange = (index: number, key: string, value: any) => {
|
||||
const newValues = [...values];
|
||||
newValues[index] = { ...newValues[index], [key]: value };
|
||||
const entry = newValues[index];
|
||||
|
||||
// If this is an existing secret and we're editing the value, set isEditing to true
|
||||
if (key === 'value' && entry.hint && value !== entry.hint) {
|
||||
newValues[index] = { ...entry, [key]: value, isEditing: true };
|
||||
} else {
|
||||
newValues[index] = { ...entry, [key]: value };
|
||||
}
|
||||
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing entry (has an ID), notify about updates
|
||||
if (entry.id && onUpdate) {
|
||||
onUpdate([newValues[index]]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteToggle = (index: number) => {
|
||||
const newValues = [...values];
|
||||
const entry = newValues[index];
|
||||
const newDeletedState = !entry.deleted;
|
||||
|
||||
newValues[index] = { ...entry, deleted: newDeletedState };
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing env var with an ID, notify parent of deleted IDs
|
||||
if (entry.id && onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const entry = values[index];
|
||||
|
||||
if (entry.id) {
|
||||
// For existing entries, mark as deleted
|
||||
const newValues = [...values];
|
||||
newValues[index] = { ...entry, deleted: true };
|
||||
setValues(newValues);
|
||||
|
||||
if (onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
} else {
|
||||
// For new entries, remove completely
|
||||
const newValues = values.filter((_, i) => i !== index);
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd(newValues.filter((v) => !v.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && <div className="mb-2 text-white">{label}</div>}
|
||||
{values?.map((entry: KeyValueType, i: number) => {
|
||||
if (!entry.deleted) {
|
||||
return (
|
||||
<div className="mb-2 flex items-center" key={i}>
|
||||
<Input
|
||||
placeholder="ex: key"
|
||||
value={entry.key}
|
||||
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'w-64',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
<div className="mx-2" />
|
||||
{entry.hidden ? (
|
||||
<Input
|
||||
placeholder="ex: value"
|
||||
value={entry.value}
|
||||
onChange={(e) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
type="password"
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder="ex: value"
|
||||
value={entry.value}
|
||||
onChange={(e: any) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
rows={entry.value?.split('\n').length || 0}
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{values?.map((entry: KeyValueType, i: number) => (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex items-center gap-2',
|
||||
entry.deleted && 'opacity-50 [&>*]:line-through',
|
||||
)}
|
||||
key={i}
|
||||
>
|
||||
<Input
|
||||
placeholder="ex: key"
|
||||
value={entry.key}
|
||||
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'w-64',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{entry.hidden ? (
|
||||
<Input
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e) => handleValueChange(i, 'value', e.target.value)}
|
||||
type="password"
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
{secretOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
!entry.locked &&
|
||||
handleValueChange(i, 'hidden', !entry.hidden)
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e: any) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
rows={entry.value?.split('\n').length || 0}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{secretOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
!entry.locked && handleValueChange(i, 'hidden', !entry.hidden)
|
||||
}
|
||||
disabled={entry.locked || entry.deleted}
|
||||
>
|
||||
{entry.hidden ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
)}
|
||||
{!disabled && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (entry.id) {
|
||||
handleDeleteToggle(i);
|
||||
} else {
|
||||
handleRemove(i);
|
||||
}
|
||||
disabled={entry.locked}
|
||||
>
|
||||
{entry.hidden ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
)}
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValues = values.filter((_, index) => index !== i);
|
||||
setValues(newValues);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!disabled && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const newValues = [
|
||||
...values,
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
},
|
||||
];
|
||||
const newEntry = {
|
||||
key: '',
|
||||
value: '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
};
|
||||
const newValues = [...values, newEntry];
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd([...values.filter((v) => !v.id), newEntry]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add row
|
||||
|
||||
@@ -11,6 +11,9 @@ export type KeyValueType = {
|
||||
hidden: boolean;
|
||||
locked: boolean;
|
||||
deleted: boolean;
|
||||
id?: string; // Optional ID for existing env vars
|
||||
isEditing?: boolean; // Whether the value is being edited
|
||||
hint?: string; // Hint value for existing secrets
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
@@ -20,6 +23,9 @@ type PropsType = {
|
||||
disabled?: boolean;
|
||||
fileUpload?: boolean;
|
||||
secretOption?: boolean;
|
||||
onDeleteIds?: (ids: string[]) => void; // Callback for deleted IDs
|
||||
onAdd?: (values: KeyValueType[]) => void; // Callback for added values
|
||||
onUpdate?: (values: KeyValueType[]) => void; // Callback for updated values
|
||||
};
|
||||
|
||||
const EnvGroupArray: React.FC<PropsType> = ({
|
||||
@@ -28,6 +34,9 @@ const EnvGroupArray: React.FC<PropsType> = ({
|
||||
setValues = () => {},
|
||||
disabled,
|
||||
secretOption,
|
||||
onDeleteIds,
|
||||
onAdd,
|
||||
onUpdate,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!values) {
|
||||
@@ -37,102 +46,164 @@ const EnvGroupArray: React.FC<PropsType> = ({
|
||||
|
||||
const handleValueChange = (index: number, key: string, value: any) => {
|
||||
const newValues = [...values];
|
||||
newValues[index] = { ...newValues[index], [key]: value };
|
||||
const entry = newValues[index];
|
||||
|
||||
// If this is an existing secret and we're editing the value, set isEditing to true
|
||||
if (key === 'value' && entry.hint && value !== entry.hint) {
|
||||
newValues[index] = { ...entry, [key]: value, isEditing: true };
|
||||
} else {
|
||||
newValues[index] = { ...entry, [key]: value };
|
||||
}
|
||||
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing entry (has an ID), notify about updates
|
||||
if (entry.id && onUpdate) {
|
||||
onUpdate([newValues[index]]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteToggle = (index: number) => {
|
||||
const newValues = [...values];
|
||||
const entry = newValues[index];
|
||||
const newDeletedState = !entry.deleted;
|
||||
|
||||
newValues[index] = { ...entry, deleted: newDeletedState };
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing env var with an ID, notify parent of deleted IDs
|
||||
if (entry.id && onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const entry = values[index];
|
||||
|
||||
if (entry.id) {
|
||||
// For existing entries, mark as deleted
|
||||
const newValues = [...values];
|
||||
newValues[index] = { ...entry, deleted: true };
|
||||
setValues(newValues);
|
||||
|
||||
if (onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
} else {
|
||||
// For new entries, remove completely
|
||||
const newValues = values.filter((_, i) => i !== index);
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd(newValues.filter((v) => !v.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && <div className="mb-2 text-white">{label}</div>}
|
||||
{values?.map((entry: KeyValueType, i: number) => {
|
||||
if (!entry.deleted) {
|
||||
return (
|
||||
<div className="mb-2 flex items-center" key={i}>
|
||||
<Input
|
||||
placeholder="ex: key"
|
||||
value={entry.key}
|
||||
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'w-64',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
<div className="mx-2" />
|
||||
{entry.hidden ? (
|
||||
<Input
|
||||
placeholder="ex: value"
|
||||
value={entry.value}
|
||||
onChange={(e) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
type="password"
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder="ex: value"
|
||||
value={entry.value}
|
||||
onChange={(e: any) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
rows={entry.value?.split('\n').length || 0}
|
||||
disabled={disabled || entry.locked}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{values?.map((entry: KeyValueType, i: number) => (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex items-center gap-2',
|
||||
entry.deleted && 'opacity-50 [&>*]:line-through',
|
||||
)}
|
||||
key={i}
|
||||
>
|
||||
<Input
|
||||
placeholder="ex: key"
|
||||
value={entry.key}
|
||||
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'w-64',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{entry.hidden ? (
|
||||
<Input
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e) => handleValueChange(i, 'value', e.target.value)}
|
||||
type="password"
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
{secretOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
!entry.locked &&
|
||||
handleValueChange(i, 'hidden', !entry.hidden)
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e: any) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
rows={entry.value?.split('\n').length || 0}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{secretOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
!entry.locked && handleValueChange(i, 'hidden', !entry.hidden)
|
||||
}
|
||||
disabled={entry.locked || entry.deleted}
|
||||
>
|
||||
{entry.hidden ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
)}
|
||||
{!disabled && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (entry.id) {
|
||||
handleDeleteToggle(i);
|
||||
} else {
|
||||
handleRemove(i);
|
||||
}
|
||||
disabled={entry.locked}
|
||||
>
|
||||
{entry.hidden ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
)}
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValues = values.filter((_, index) => index !== i);
|
||||
setValues(newValues);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!disabled && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const newValues = [
|
||||
...values,
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
},
|
||||
];
|
||||
const newEntry = {
|
||||
key: '',
|
||||
value: '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
};
|
||||
const newValues = [...values, newEntry];
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd([...values.filter((v) => !v.id), newEntry]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add row
|
||||
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
} from "./data-contracts";
|
||||
import { ContentType, HttpClient, RequestParams } from "./http-client";
|
||||
|
||||
export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
|
||||
export class Api<
|
||||
SecurityDataType = unknown,
|
||||
> extends HttpClient<SecurityDataType> {
|
||||
/**
|
||||
* @description Gets metadata for the Hatchet instance
|
||||
*
|
||||
@@ -262,7 +264,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request POST:/api/v1/cloud/tenants/{tenant}/managed-worker
|
||||
* @secure
|
||||
*/
|
||||
managedWorkerCreate = (tenant: string, data: CreateManagedWorkerRequest, params: RequestParams = {}) =>
|
||||
managedWorkerCreate = (
|
||||
tenant: string,
|
||||
data: CreateManagedWorkerRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<ManagedWorker, APIErrors>({
|
||||
path: `/api/v1/cloud/tenants/${tenant}/managed-worker`,
|
||||
method: "POST",
|
||||
@@ -338,7 +344,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request POST:/api/v1/cloud/managed-worker/{managed-worker}
|
||||
* @secure
|
||||
*/
|
||||
managedWorkerUpdate = (managedWorker: string, data: UpdateManagedWorkerRequest, params: RequestParams = {}) =>
|
||||
managedWorkerUpdate = (
|
||||
managedWorker: string,
|
||||
data: UpdateManagedWorkerRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<ManagedWorker, APIErrors>({
|
||||
path: `/api/v1/cloud/managed-worker/${managedWorker}`,
|
||||
method: "POST",
|
||||
@@ -374,7 +384,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request POST:/api/v1/cloud/infra-as-code/{infra-as-code-request}
|
||||
* @secure
|
||||
*/
|
||||
infraAsCodeCreate = (infraAsCodeRequest: string, data: InfraAsCodeRequest, params: RequestParams = {}) =>
|
||||
infraAsCodeCreate = (
|
||||
infraAsCodeRequest: string,
|
||||
data: InfraAsCodeRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<void, APIErrors>({
|
||||
path: `/api/v1/cloud/infra-as-code/${infraAsCodeRequest}`,
|
||||
method: "POST",
|
||||
@@ -392,7 +406,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request GET:/api/v1/cloud/runtime-config/{runtime-config}/actions
|
||||
* @secure
|
||||
*/
|
||||
runtimeConfigListActions = (runtimeConfig: string, params: RequestParams = {}) =>
|
||||
runtimeConfigListActions = (
|
||||
runtimeConfig: string,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<RuntimeConfigActionsResponse, APIErrors>({
|
||||
path: `/api/v1/cloud/runtime-config/${runtimeConfig}/actions`,
|
||||
method: "GET",
|
||||
@@ -570,7 +587,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request GET:/api/v1/cloud/managed-worker/{managed-worker}/instances
|
||||
* @secure
|
||||
*/
|
||||
managedWorkerInstancesList = (managedWorker: string, params: RequestParams = {}) =>
|
||||
managedWorkerInstancesList = (
|
||||
managedWorker: string,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<InstanceList, APIErrors>({
|
||||
path: `/api/v1/cloud/managed-worker/${managedWorker}/instances`,
|
||||
method: "GET",
|
||||
@@ -621,7 +641,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request GET:/api/v1/cloud/managed-worker/{managed-worker}/events
|
||||
* @secure
|
||||
*/
|
||||
managedWorkerEventsList = (managedWorker: string, params: RequestParams = {}) =>
|
||||
managedWorkerEventsList = (
|
||||
managedWorker: string,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<ManagedWorkerEventList, APIErrors>({
|
||||
path: `/api/v1/cloud/managed-worker/${managedWorker}/events`,
|
||||
method: "GET",
|
||||
@@ -669,7 +692,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request PATCH:/api/v1/billing/tenants/{tenant}/subscription
|
||||
* @secure
|
||||
*/
|
||||
subscriptionUpsert = (tenant: string, data: UpdateTenantSubscription, params: RequestParams = {}) =>
|
||||
subscriptionUpsert = (
|
||||
tenant: string,
|
||||
data: UpdateTenantSubscription,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<TenantSubscription, APIErrors>({
|
||||
path: `/api/v1/billing/tenants/${tenant}/subscription`,
|
||||
method: "PATCH",
|
||||
@@ -711,7 +738,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
* @request POST:/api/v1/cloud/tenants/{tenant}/logs
|
||||
* @secure
|
||||
*/
|
||||
logCreate = (tenant: string, data: VectorPushRequest, params: RequestParams = {}) =>
|
||||
logCreate = (
|
||||
tenant: string,
|
||||
data: VectorPushRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<void, APIErrors>({
|
||||
path: `/api/v1/cloud/tenants/${tenant}/logs`,
|
||||
method: "POST",
|
||||
|
||||
@@ -10,6 +10,72 @@
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export enum TemplateOptions {
|
||||
QUICKSTART_PYTHON = "QUICKSTART_PYTHON",
|
||||
QUICKSTART_TYPESCRIPT = "QUICKSTART_TYPESCRIPT",
|
||||
QUICKSTART_GO = "QUICKSTART_GO",
|
||||
}
|
||||
|
||||
export enum AutoscalingTargetKind {
|
||||
PORTER = "PORTER",
|
||||
FLY = "FLY",
|
||||
}
|
||||
|
||||
export enum CouponFrequency {
|
||||
Once = "once",
|
||||
Recurring = "recurring",
|
||||
}
|
||||
|
||||
export enum TenantSubscriptionStatus {
|
||||
Active = "active",
|
||||
Pending = "pending",
|
||||
Terminated = "terminated",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
export enum ManagedWorkerRegion {
|
||||
Ams = "ams",
|
||||
Arn = "arn",
|
||||
Atl = "atl",
|
||||
Bog = "bog",
|
||||
Bos = "bos",
|
||||
Cdg = "cdg",
|
||||
Den = "den",
|
||||
Dfw = "dfw",
|
||||
Ewr = "ewr",
|
||||
Eze = "eze",
|
||||
Gdl = "gdl",
|
||||
Gig = "gig",
|
||||
Gru = "gru",
|
||||
Hkg = "hkg",
|
||||
Iad = "iad",
|
||||
Jnb = "jnb",
|
||||
Lax = "lax",
|
||||
Lhr = "lhr",
|
||||
Mad = "mad",
|
||||
Mia = "mia",
|
||||
Nrt = "nrt",
|
||||
Ord = "ord",
|
||||
Otp = "otp",
|
||||
Phx = "phx",
|
||||
Qro = "qro",
|
||||
Scl = "scl",
|
||||
Sea = "sea",
|
||||
Sin = "sin",
|
||||
Sjc = "sjc",
|
||||
Syd = "syd",
|
||||
Waw = "waw",
|
||||
Yul = "yul",
|
||||
Yyz = "yyz",
|
||||
}
|
||||
|
||||
export enum ManagedWorkerEventStatus {
|
||||
IN_PROGRESS = "IN_PROGRESS",
|
||||
SUCCEEDED = "SUCCEEDED",
|
||||
FAILED = "FAILED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export interface APICloudMetadata {
|
||||
/**
|
||||
* whether the tenant can be billed
|
||||
@@ -138,8 +204,8 @@ export interface ManagedWorker {
|
||||
name: string;
|
||||
buildConfig?: ManagedWorkerBuildConfig;
|
||||
isIac: boolean;
|
||||
/** A map of environment variables to set for the worker */
|
||||
envVars: Record<string, string>;
|
||||
directSecrets: ManagedWorkerSecret[];
|
||||
globalSecrets: ManagedWorkerSecret[];
|
||||
runtimeConfigs?: ManagedWorkerRuntimeConfig[];
|
||||
}
|
||||
|
||||
@@ -161,6 +227,46 @@ export interface ManagedWorkerBuildConfig {
|
||||
steps?: BuildStep[];
|
||||
}
|
||||
|
||||
export interface ManagedWorkerSecret {
|
||||
key: string;
|
||||
id: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
export interface CreateManagedWorkerSecretRequest {
|
||||
/** array of secret keys and values to add to the worker */
|
||||
add?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
/** array of global secret ids to add to the worker */
|
||||
addGlobal?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateManagedWorkerSecretRequest {
|
||||
/** array of secret keys and values to add to the worker */
|
||||
add?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
/** array of global secret ids to add to the worker */
|
||||
addGlobal?: string[];
|
||||
/** array of secret ids to delete from the worker */
|
||||
delete?: string[];
|
||||
/** array of existing secret ids and values to update in the worker */
|
||||
update?: {
|
||||
/**
|
||||
* @format uuid
|
||||
* @minLength 36
|
||||
* @maxLength 36
|
||||
*/
|
||||
id: string;
|
||||
/** @minLength 1 */
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface BuildStep {
|
||||
metadata: APIResourceMeta;
|
||||
/** The relative path to the build directory */
|
||||
@@ -185,13 +291,6 @@ export interface ManagedWorkerRuntimeConfig {
|
||||
actions?: string[];
|
||||
}
|
||||
|
||||
export enum ManagedWorkerEventStatus {
|
||||
IN_PROGRESS = "IN_PROGRESS",
|
||||
SUCCEEDED = "SUCCEEDED",
|
||||
FAILED = "FAILED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export interface ManagedWorkerEvent {
|
||||
id: number;
|
||||
/** @format date-time */
|
||||
@@ -212,8 +311,7 @@ export interface ManagedWorkerEventList {
|
||||
export interface CreateManagedWorkerRequest {
|
||||
name: string;
|
||||
buildConfig: CreateManagedWorkerBuildConfigRequest;
|
||||
/** A map of environment variables to set for the worker */
|
||||
envVars: Record<string, string>;
|
||||
secrets?: CreateManagedWorkerSecretRequest;
|
||||
isIac: boolean;
|
||||
runtimeConfig?: CreateManagedWorkerRuntimeConfigRequest;
|
||||
}
|
||||
@@ -221,8 +319,7 @@ export interface CreateManagedWorkerRequest {
|
||||
export interface UpdateManagedWorkerRequest {
|
||||
name?: string;
|
||||
buildConfig?: CreateManagedWorkerBuildConfigRequest;
|
||||
/** A map of environment variables to set for the worker */
|
||||
envVars?: Record<string, string>;
|
||||
secrets?: UpdateManagedWorkerSecretRequest;
|
||||
isIac?: boolean;
|
||||
runtimeConfig?: CreateManagedWorkerRuntimeConfigRequest;
|
||||
}
|
||||
@@ -255,42 +352,6 @@ export interface CreateBuildStepRequest {
|
||||
dockerfilePath: string;
|
||||
}
|
||||
|
||||
export enum ManagedWorkerRegion {
|
||||
Ams = "ams",
|
||||
Arn = "arn",
|
||||
Atl = "atl",
|
||||
Bog = "bog",
|
||||
Bos = "bos",
|
||||
Cdg = "cdg",
|
||||
Den = "den",
|
||||
Dfw = "dfw",
|
||||
Ewr = "ewr",
|
||||
Eze = "eze",
|
||||
Gdl = "gdl",
|
||||
Gig = "gig",
|
||||
Gru = "gru",
|
||||
Hkg = "hkg",
|
||||
Iad = "iad",
|
||||
Jnb = "jnb",
|
||||
Lax = "lax",
|
||||
Lhr = "lhr",
|
||||
Mad = "mad",
|
||||
Mia = "mia",
|
||||
Nrt = "nrt",
|
||||
Ord = "ord",
|
||||
Otp = "otp",
|
||||
Phx = "phx",
|
||||
Qro = "qro",
|
||||
Scl = "scl",
|
||||
Sea = "sea",
|
||||
Sin = "sin",
|
||||
Sjc = "sjc",
|
||||
Syd = "syd",
|
||||
Waw = "waw",
|
||||
Yul = "yul",
|
||||
Yyz = "yyz",
|
||||
}
|
||||
|
||||
export interface CreateManagedWorkerRuntimeConfigRequest {
|
||||
/**
|
||||
* @min 0
|
||||
@@ -382,13 +443,6 @@ export interface TenantPaymentMethod {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export enum TenantSubscriptionStatus {
|
||||
Active = "active",
|
||||
Pending = "pending",
|
||||
Terminated = "terminated",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
export interface Coupon {
|
||||
/** The name of the coupon. */
|
||||
name: string;
|
||||
@@ -408,11 +462,6 @@ export interface Coupon {
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
export enum CouponFrequency {
|
||||
Once = "once",
|
||||
Recurring = "recurring",
|
||||
}
|
||||
|
||||
export type VectorPushRequest = EventObject[];
|
||||
|
||||
export interface EventObject {
|
||||
@@ -557,11 +606,6 @@ export interface AutoscalingConfig {
|
||||
scaleToZero: boolean;
|
||||
}
|
||||
|
||||
export enum AutoscalingTargetKind {
|
||||
PORTER = "PORTER",
|
||||
FLY = "FLY",
|
||||
}
|
||||
|
||||
export interface CreateOrUpdateAutoscalingRequest {
|
||||
waitDuration: string;
|
||||
rollingWindowDuration: string;
|
||||
@@ -589,12 +633,6 @@ export interface CreateFlyAutoscalingRequest {
|
||||
currentReplicas: number;
|
||||
}
|
||||
|
||||
export enum TemplateOptions {
|
||||
QUICKSTART_PYTHON = "QUICKSTART_PYTHON",
|
||||
QUICKSTART_TYPESCRIPT = "QUICKSTART_TYPESCRIPT",
|
||||
QUICKSTART_GO = "QUICKSTART_GO",
|
||||
}
|
||||
|
||||
export interface CreateManagedWorkerFromTemplateRequest {
|
||||
name: TemplateOptions;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,19 @@
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios";
|
||||
import type {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
HeadersDefaults,
|
||||
ResponseType,
|
||||
} from "axios";
|
||||
import axios from "axios";
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
|
||||
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
|
||||
export interface FullRequestParams
|
||||
extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
|
||||
/** set parameter to `true` for call `securityWorker` for this request */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
@@ -30,9 +37,13 @@ export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "pa
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
||||
export type RequestParams = Omit<
|
||||
FullRequestParams,
|
||||
"body" | "method" | "query" | "path"
|
||||
>;
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
|
||||
export interface ApiConfig<SecurityDataType = unknown>
|
||||
extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
|
||||
securityWorker?: (
|
||||
securityData: SecurityDataType | null,
|
||||
) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
|
||||
@@ -54,8 +65,16 @@ export class HttpClient<SecurityDataType = unknown> {
|
||||
private secure?: boolean;
|
||||
private format?: ResponseType;
|
||||
|
||||
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
|
||||
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "" });
|
||||
constructor({
|
||||
securityWorker,
|
||||
secure,
|
||||
format,
|
||||
...axiosConfig
|
||||
}: ApiConfig<SecurityDataType> = {}) {
|
||||
this.instance = axios.create({
|
||||
...axiosConfig,
|
||||
baseURL: axiosConfig.baseURL || "",
|
||||
});
|
||||
this.secure = secure;
|
||||
this.format = format;
|
||||
this.securityWorker = securityWorker;
|
||||
@@ -65,7 +84,10 @@ export class HttpClient<SecurityDataType = unknown> {
|
||||
this.securityData = data;
|
||||
};
|
||||
|
||||
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
|
||||
protected mergeRequestParams(
|
||||
params1: AxiosRequestConfig,
|
||||
params2?: AxiosRequestConfig,
|
||||
): AxiosRequestConfig {
|
||||
const method = params1.method || (params2 && params2.method);
|
||||
|
||||
return {
|
||||
@@ -73,7 +95,11 @@ export class HttpClient<SecurityDataType = unknown> {
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: {
|
||||
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
|
||||
...((method &&
|
||||
this.instance.defaults.headers[
|
||||
method.toLowerCase() as keyof HeadersDefaults
|
||||
]) ||
|
||||
{}),
|
||||
...(params1.headers || {}),
|
||||
...((params2 && params2.headers) || {}),
|
||||
},
|
||||
@@ -94,11 +120,15 @@ export class HttpClient<SecurityDataType = unknown> {
|
||||
}
|
||||
return Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
const propertyContent: any[] = property instanceof Array ? property : [property];
|
||||
const propertyContent: any[] =
|
||||
property instanceof Array ? property : [property];
|
||||
|
||||
for (const formItem of propertyContent) {
|
||||
const isFileType = formItem instanceof Blob || formItem instanceof File;
|
||||
formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem));
|
||||
formData.append(
|
||||
key,
|
||||
isFileType ? formItem : this.stringifyFormItem(formItem),
|
||||
);
|
||||
}
|
||||
|
||||
return formData;
|
||||
@@ -122,11 +152,21 @@ export class HttpClient<SecurityDataType = unknown> {
|
||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||
const responseFormat = format || this.format || undefined;
|
||||
|
||||
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
|
||||
if (
|
||||
type === ContentType.FormData &&
|
||||
body &&
|
||||
body !== null &&
|
||||
typeof body === "object"
|
||||
) {
|
||||
body = this.createFormData(body as Record<string, unknown>);
|
||||
}
|
||||
|
||||
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
|
||||
if (
|
||||
type === ContentType.Text &&
|
||||
body &&
|
||||
body !== null &&
|
||||
typeof body !== "string"
|
||||
) {
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
|
||||
+93
-31
@@ -16,7 +16,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Label } from '@/components/v1/ui/label';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
|
||||
import { Input } from '@/components/v1/ui/input';
|
||||
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
|
||||
import {
|
||||
getRepoName,
|
||||
getRepoOwner,
|
||||
@@ -49,6 +48,7 @@ import {
|
||||
managedCompute,
|
||||
} from '@/lib/can/features/managed-compute';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
|
||||
|
||||
interface UpdateWorkerFormProps {
|
||||
onSubmit: (opts: z.infer<typeof updateManagedWorkerSchema>) => void;
|
||||
@@ -75,7 +75,22 @@ const updateManagedWorkerSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
isIac: z.boolean().default(false).optional(),
|
||||
envVars: z.record(z.string()).optional(),
|
||||
secrets: z.object({
|
||||
add: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string()),
|
||||
}),
|
||||
runtimeConfig: z
|
||||
.object({
|
||||
numReplicas: z.number().min(0).max(16).optional(),
|
||||
@@ -139,7 +154,11 @@ export default function UpdateWorkerForm({
|
||||
},
|
||||
],
|
||||
},
|
||||
envVars: managedWorker.envVars,
|
||||
secrets: {
|
||||
add: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
isIac: managedWorker.isIac,
|
||||
runtimeConfig:
|
||||
!managedWorker.isIac && managedWorker.runtimeConfigs?.length == 1
|
||||
@@ -193,6 +212,25 @@ export default function UpdateWorkerForm({
|
||||
'1 CPU, 1 GB RAM (shared CPU)',
|
||||
);
|
||||
|
||||
// Set initial machine type based on current worker configuration
|
||||
useEffect(() => {
|
||||
if (
|
||||
managedWorker?.runtimeConfigs &&
|
||||
managedWorker.runtimeConfigs.length > 0
|
||||
) {
|
||||
const config = managedWorker.runtimeConfigs[0];
|
||||
const matchingType = machineTypes.find(
|
||||
(m) =>
|
||||
m.cpuKind === config.cpuKind &&
|
||||
m.cpus === config.cpus &&
|
||||
m.memoryMb === config.memoryMb,
|
||||
);
|
||||
if (matchingType) {
|
||||
setMachineType(matchingType.title);
|
||||
}
|
||||
}
|
||||
}, [managedWorker]);
|
||||
|
||||
const region = watch('runtimeConfig.regions');
|
||||
const installation = watch('buildConfig.githubInstallationId');
|
||||
const repoOwner = watch('buildConfig.githubRepositoryOwner');
|
||||
@@ -212,8 +250,16 @@ export default function UpdateWorkerForm({
|
||||
...queries.github.listBranches(tenantId, installation, repoOwner, repoName),
|
||||
});
|
||||
|
||||
const [envVars, setEnvVars] = useState<KeyValueType[]>(
|
||||
envVarsRecordToKeyValueType(managedWorker.envVars),
|
||||
const [secrets, setSecrets] = useState<KeyValueType[]>(
|
||||
managedWorker.directSecrets?.map((secret) => ({
|
||||
key: secret.key,
|
||||
value: secret.hint || '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
id: secret.id,
|
||||
hint: secret.hint,
|
||||
})) || [],
|
||||
);
|
||||
|
||||
const [isIac, setIsIac] = useState(managedWorker.isIac);
|
||||
@@ -234,8 +280,8 @@ export default function UpdateWorkerForm({
|
||||
const numReplicasError =
|
||||
errors.runtimeConfig?.numReplicas?.message?.toString() ||
|
||||
fieldErrors?.numReplicas;
|
||||
const envVarsError =
|
||||
errors.envVars?.message?.toString() || fieldErrors?.envVars;
|
||||
const secretsError =
|
||||
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
|
||||
const cpuKindError =
|
||||
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
|
||||
const cpusError =
|
||||
@@ -325,6 +371,33 @@ export default function UpdateWorkerForm({
|
||||
return can(managedCompute.selectCompute(computeType));
|
||||
}, [can, machineType]);
|
||||
|
||||
// Update form values when secrets change
|
||||
useEffect(() => {
|
||||
// Split secrets into add/update/delete
|
||||
const toAdd = secrets.filter((s) => !s.id && !s.deleted);
|
||||
const toUpdate = secrets.filter((s) => s.id && !s.deleted);
|
||||
const toDelete = secrets.filter((s) => s.id && s.deleted).map((s) => s.id!);
|
||||
|
||||
setValue(
|
||||
'secrets.add',
|
||||
toAdd.map((s) => ({
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
})),
|
||||
);
|
||||
|
||||
setValue(
|
||||
'secrets.update',
|
||||
toUpdate.map((s) => ({
|
||||
id: s.id!,
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
})),
|
||||
);
|
||||
|
||||
setValue('secrets.delete', toDelete);
|
||||
}, [secrets, setValue]);
|
||||
|
||||
// if there are no github accounts linked, ask the user to link one
|
||||
if (
|
||||
listInstallationsQuery.isSuccess &&
|
||||
@@ -565,20 +638,21 @@ export default function UpdateWorkerForm({
|
||||
</div>
|
||||
<Label>Environment Variables</Label>
|
||||
<EnvGroupArray
|
||||
values={envVars}
|
||||
setValues={(value) => {
|
||||
setEnvVars(value);
|
||||
setValue(
|
||||
'envVars',
|
||||
value.reduce<Record<string, string>>((acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
values={secrets}
|
||||
setValues={setSecrets}
|
||||
onUpdate={(updatedSecrets) => {
|
||||
// Update the secrets state with the new values
|
||||
setSecrets((prev) => {
|
||||
const newSecrets = prev.map((s) => {
|
||||
const updated = updatedSecrets.find((u) => u.id === s.id);
|
||||
return updated ? { ...s, ...updated } : s;
|
||||
});
|
||||
return newSecrets;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{envVarsError && (
|
||||
<div className="text-sm text-red-500">{envVarsError}</div>
|
||||
{secretsError && (
|
||||
<div className="text-sm text-red-500">{secretsError}</div>
|
||||
)}
|
||||
<Label>Machine Configuration Method</Label>
|
||||
<Tabs
|
||||
@@ -1008,15 +1082,3 @@ export default function UpdateWorkerForm({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function envVarsRecordToKeyValueType(
|
||||
envVars: Record<string, string>,
|
||||
): KeyValueType[] {
|
||||
return Object.entries(envVars).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
}));
|
||||
}
|
||||
|
||||
+23
-14
@@ -265,7 +265,14 @@ const createManagedWorkerSchema = z.object({
|
||||
),
|
||||
}),
|
||||
isIac: z.boolean().default(false),
|
||||
envVars: z.record(z.string()),
|
||||
secrets: z.object({
|
||||
add: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
runtimeConfig: z.object({
|
||||
numReplicas: z.number().min(0).max(16).optional(),
|
||||
cpuKind: z.string(),
|
||||
@@ -323,7 +330,9 @@ export default function CreateWorkerForm({
|
||||
},
|
||||
],
|
||||
},
|
||||
envVars: {},
|
||||
secrets: {
|
||||
add: [],
|
||||
},
|
||||
runtimeConfig: {
|
||||
numReplicas: 1,
|
||||
cpuKind: 'shared',
|
||||
@@ -411,7 +420,7 @@ export default function CreateWorkerForm({
|
||||
return allowed;
|
||||
}, [can, autoscalingMaxReplicas]);
|
||||
|
||||
const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
|
||||
const [secrets, setSecrets] = useState<KeyValueType[]>([]);
|
||||
const [isIac, setIsIac] = useState(false);
|
||||
const [scalingType, setScalingType] = useState<ScalingType>('Static');
|
||||
|
||||
@@ -425,8 +434,8 @@ export default function CreateWorkerForm({
|
||||
const numReplicasError =
|
||||
errors.runtimeConfig?.numReplicas?.message?.toString() ||
|
||||
fieldErrors?.numReplicas;
|
||||
const envVarsError =
|
||||
errors.envVars?.message?.toString() || fieldErrors?.envVars;
|
||||
const secretsError =
|
||||
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
|
||||
const cpuKindError =
|
||||
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
|
||||
const cpusError =
|
||||
@@ -710,20 +719,20 @@ export default function CreateWorkerForm({
|
||||
</div>
|
||||
<Label>Environment Variables</Label>
|
||||
<EnvGroupArray
|
||||
values={envVars}
|
||||
values={secrets}
|
||||
setValues={(value) => {
|
||||
setEnvVars(value);
|
||||
setSecrets(value);
|
||||
setValue(
|
||||
'envVars',
|
||||
value.reduce<Record<string, string>>((acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
'secrets.add',
|
||||
value.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{envVarsError && (
|
||||
<div className="text-sm text-red-500">{envVarsError}</div>
|
||||
{secretsError && (
|
||||
<div className="text-sm text-red-500">{secretsError}</div>
|
||||
)}
|
||||
<Label>Machine Configuration Method</Label>
|
||||
<Tabs
|
||||
|
||||
+93
-31
@@ -16,7 +16,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Label } from '@/components/v1/ui/label';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
|
||||
import { Input } from '@/components/v1/ui/input';
|
||||
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
|
||||
import {
|
||||
getRepoName,
|
||||
getRepoOwner,
|
||||
@@ -49,6 +48,7 @@ import {
|
||||
managedCompute,
|
||||
} from '@/lib/can/features/managed-compute';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
|
||||
|
||||
interface UpdateWorkerFormProps {
|
||||
onSubmit: (opts: z.infer<typeof updateManagedWorkerSchema>) => void;
|
||||
@@ -75,7 +75,22 @@ const updateManagedWorkerSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
isIac: z.boolean().default(false).optional(),
|
||||
envVars: z.record(z.string()).optional(),
|
||||
secrets: z.object({
|
||||
add: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string()),
|
||||
}),
|
||||
runtimeConfig: z
|
||||
.object({
|
||||
numReplicas: z.number().min(0).max(16).optional(),
|
||||
@@ -139,7 +154,11 @@ export default function UpdateWorkerForm({
|
||||
},
|
||||
],
|
||||
},
|
||||
envVars: managedWorker.envVars,
|
||||
secrets: {
|
||||
add: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
isIac: managedWorker.isIac,
|
||||
runtimeConfig:
|
||||
!managedWorker.isIac && managedWorker.runtimeConfigs?.length == 1
|
||||
@@ -193,6 +212,25 @@ export default function UpdateWorkerForm({
|
||||
'1 CPU, 1 GB RAM (shared CPU)',
|
||||
);
|
||||
|
||||
// Set initial machine type based on current worker configuration
|
||||
useEffect(() => {
|
||||
if (
|
||||
managedWorker?.runtimeConfigs &&
|
||||
managedWorker.runtimeConfigs.length > 0
|
||||
) {
|
||||
const config = managedWorker.runtimeConfigs[0];
|
||||
const matchingType = machineTypes.find(
|
||||
(m) =>
|
||||
m.cpuKind === config.cpuKind &&
|
||||
m.cpus === config.cpus &&
|
||||
m.memoryMb === config.memoryMb,
|
||||
);
|
||||
if (matchingType) {
|
||||
setMachineType(matchingType.title);
|
||||
}
|
||||
}
|
||||
}, [managedWorker]);
|
||||
|
||||
const region = watch('runtimeConfig.regions');
|
||||
const installation = watch('buildConfig.githubInstallationId');
|
||||
const repoOwner = watch('buildConfig.githubRepositoryOwner');
|
||||
@@ -212,8 +250,16 @@ export default function UpdateWorkerForm({
|
||||
...queries.github.listBranches(tenantId, installation, repoOwner, repoName),
|
||||
});
|
||||
|
||||
const [envVars, setEnvVars] = useState<KeyValueType[]>(
|
||||
envVarsRecordToKeyValueType(managedWorker.envVars),
|
||||
const [secrets, setSecrets] = useState<KeyValueType[]>(
|
||||
managedWorker.directSecrets?.map((secret) => ({
|
||||
key: secret.key,
|
||||
value: secret.hint || '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
id: secret.id,
|
||||
hint: secret.hint,
|
||||
})) || [],
|
||||
);
|
||||
|
||||
const [isIac, setIsIac] = useState(managedWorker.isIac);
|
||||
@@ -234,8 +280,8 @@ export default function UpdateWorkerForm({
|
||||
const numReplicasError =
|
||||
errors.runtimeConfig?.numReplicas?.message?.toString() ||
|
||||
fieldErrors?.numReplicas;
|
||||
const envVarsError =
|
||||
errors.envVars?.message?.toString() || fieldErrors?.envVars;
|
||||
const secretsError =
|
||||
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
|
||||
const cpuKindError =
|
||||
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
|
||||
const cpusError =
|
||||
@@ -325,6 +371,33 @@ export default function UpdateWorkerForm({
|
||||
return can(managedCompute.selectCompute(computeType));
|
||||
}, [can, machineType]);
|
||||
|
||||
// Update form values when secrets change
|
||||
useEffect(() => {
|
||||
// Split secrets into add/update/delete
|
||||
const toAdd = secrets.filter((s) => !s.id && !s.deleted);
|
||||
const toUpdate = secrets.filter((s) => s.id && !s.deleted);
|
||||
const toDelete = secrets.filter((s) => s.id && s.deleted).map((s) => s.id!);
|
||||
|
||||
setValue(
|
||||
'secrets.add',
|
||||
toAdd.map((s) => ({
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
})),
|
||||
);
|
||||
|
||||
setValue(
|
||||
'secrets.update',
|
||||
toUpdate.map((s) => ({
|
||||
id: s.id!,
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
})),
|
||||
);
|
||||
|
||||
setValue('secrets.delete', toDelete);
|
||||
}, [secrets, setValue]);
|
||||
|
||||
// if there are no github accounts linked, ask the user to link one
|
||||
if (
|
||||
listInstallationsQuery.isSuccess &&
|
||||
@@ -565,20 +638,21 @@ export default function UpdateWorkerForm({
|
||||
</div>
|
||||
<Label>Environment Variables</Label>
|
||||
<EnvGroupArray
|
||||
values={envVars}
|
||||
setValues={(value) => {
|
||||
setEnvVars(value);
|
||||
setValue(
|
||||
'envVars',
|
||||
value.reduce<Record<string, string>>((acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
values={secrets}
|
||||
setValues={setSecrets}
|
||||
onUpdate={(updatedSecrets) => {
|
||||
// Update the secrets state with the new values
|
||||
setSecrets((prev) => {
|
||||
const newSecrets = prev.map((s) => {
|
||||
const updated = updatedSecrets.find((u) => u.id === s.id);
|
||||
return updated ? { ...s, ...updated } : s;
|
||||
});
|
||||
return newSecrets;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{envVarsError && (
|
||||
<div className="text-sm text-red-500">{envVarsError}</div>
|
||||
{secretsError && (
|
||||
<div className="text-sm text-red-500">{secretsError}</div>
|
||||
)}
|
||||
<Label>Machine Configuration Method</Label>
|
||||
<Tabs
|
||||
@@ -1008,15 +1082,3 @@ export default function UpdateWorkerForm({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function envVarsRecordToKeyValueType(
|
||||
envVars: Record<string, string>,
|
||||
): KeyValueType[] {
|
||||
return Object.entries(envVars).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
}));
|
||||
}
|
||||
|
||||
+24
-15
@@ -265,7 +265,14 @@ const createManagedWorkerSchema = z.object({
|
||||
),
|
||||
}),
|
||||
isIac: z.boolean().default(false),
|
||||
envVars: z.record(z.string()),
|
||||
secrets: z.object({
|
||||
add: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
runtimeConfig: z.object({
|
||||
numReplicas: z.number().min(0).max(16).optional(),
|
||||
cpuKind: z.string(),
|
||||
@@ -323,7 +330,9 @@ export default function CreateWorkerForm({
|
||||
},
|
||||
],
|
||||
},
|
||||
envVars: {},
|
||||
secrets: {
|
||||
add: [],
|
||||
},
|
||||
runtimeConfig: {
|
||||
numReplicas: 1,
|
||||
cpuKind: 'shared',
|
||||
@@ -411,7 +420,7 @@ export default function CreateWorkerForm({
|
||||
return allowed;
|
||||
}, [can, autoscalingMaxReplicas]);
|
||||
|
||||
const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
|
||||
const [secrets, setSecrets] = useState<KeyValueType[]>([]);
|
||||
const [isIac, setIsIac] = useState(false);
|
||||
const [scalingType, setScalingType] = useState<ScalingType>('Static');
|
||||
|
||||
@@ -425,8 +434,8 @@ export default function CreateWorkerForm({
|
||||
const numReplicasError =
|
||||
errors.runtimeConfig?.numReplicas?.message?.toString() ||
|
||||
fieldErrors?.numReplicas;
|
||||
const envVarsError =
|
||||
errors.envVars?.message?.toString() || fieldErrors?.envVars;
|
||||
const secretsError =
|
||||
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
|
||||
const cpuKindError =
|
||||
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
|
||||
const cpusError =
|
||||
@@ -710,20 +719,20 @@ export default function CreateWorkerForm({
|
||||
</div>
|
||||
<Label>Environment Variables</Label>
|
||||
<EnvGroupArray
|
||||
values={envVars}
|
||||
values={secrets}
|
||||
setValues={(value) => {
|
||||
setEnvVars(value);
|
||||
setSecrets(value);
|
||||
setValue(
|
||||
'envVars',
|
||||
value.reduce<Record<string, string>>((acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
'secrets.add',
|
||||
value.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{envVarsError && (
|
||||
<div className="text-sm text-red-500">{envVarsError}</div>
|
||||
{secretsError && (
|
||||
<div className="text-sm text-red-500">{secretsError}</div>
|
||||
)}
|
||||
<Label>Machine Configuration Method</Label>
|
||||
<Tabs
|
||||
@@ -1290,7 +1299,7 @@ export function getRepoName(repoOwnerName?: string) {
|
||||
// URL for the billing portal to upgrade
|
||||
const getBillingPortalUrl = () => {
|
||||
// Replace with your actual billing portal URL or API call
|
||||
return '/v1/tenant-settings/billing-and-limits';
|
||||
return '/tenant-settings/billing-and-limits';
|
||||
};
|
||||
|
||||
export const UpgradeMessage = ({ feature }: { feature: string }) => (
|
||||
|
||||
@@ -486,7 +486,7 @@ func createControllerLayer(dc *database.Layer, cf *server.ServerConfigFile, vers
|
||||
})
|
||||
}
|
||||
|
||||
encryptionSvc, err := loadEncryptionSvc(cf)
|
||||
encryptionSvc, err := LoadEncryptionSvc(cf)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not load encryption service: %w", err)
|
||||
@@ -618,7 +618,7 @@ func getStrArr(v string) []string {
|
||||
return strings.Split(v, " ")
|
||||
}
|
||||
|
||||
func loadEncryptionSvc(cf *server.ServerConfigFile) (encryption.EncryptionService, error) {
|
||||
func LoadEncryptionSvc(cf *server.ServerConfigFile) (encryption.EncryptionService, error) {
|
||||
var err error
|
||||
|
||||
hasLocalMasterKeyset := cf.Encryption.MasterKeyset != "" || cf.Encryption.MasterKeysetFile != ""
|
||||
|
||||
Reference in New Issue
Block a user