Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 27 additions & 19 deletions apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useSdlServiceManager } from "@src/hooks/useSdlServiceManager/useSdlServ
import { useGpuModels } from "@src/queries/useGpuQuery";
import type { SdlBuilderFormValuesType, ServiceType } from "@src/types";
import { SdlBuilderFormValuesSchema } from "@src/types";
import { getDefaultService } from "@src/utils/sdl/data";
import { defaultServiceWithPlacement, sshServiceOverrides } from "@src/utils/sdl/data";
import { generateSdl } from "@src/utils/sdl/sdlGenerator";
import { importSimpleSdl } from "@src/utils/sdl/sdlImport";
import { transformCustomSdlFields, TransformError } from "@src/utils/sdl/transformCustomSdlFields";
Expand Down Expand Up @@ -52,12 +52,13 @@ export const SdlBuilder = React.forwardRef<SdlBuilderRefType, Props>(
const formRef = useRef<HTMLFormElement>(null);
const [isInit, setIsInit] = useState(false);
const { hasComponent, imageList } = d.useSdlBuilder();
const initialValues = useRef<SdlBuilderFormValuesType>({
...defaultServiceWithPlacement(hasComponent("ssh") ? sshServiceOverrides : undefined),
imageList: imageList,
hasSSHKey: hasComponent("ssh")
}).current;
const form = useForm<SdlBuilderFormValuesType>({
defaultValues: {
services: [getDefaultService({ supportsSSH: hasComponent("ssh") })],
imageList: imageList,
hasSSHKey: hasComponent("ssh")
},
defaultValues: initialValues,
resolver: zodResolver(SdlBuilderFormValuesSchema)
});
const { control, trigger, watch, setValue, formState } = form;
Expand All @@ -71,10 +72,10 @@ export const SdlBuilder = React.forwardRef<SdlBuilderRefType, Props>(

useEffect(() => {
formServices.forEach((service, index) => {
const { denom } = service.placement.pricing;
const { denom } = service.pricing;

if (denom !== wallet.denom) {
setValue(`services.${index}.placement.pricing.denom`, wallet.denom);
setValue(`services.${index}.pricing.denom`, wallet.denom);
}
});
}, [formServices, sdlString, wallet.denom]);
Expand All @@ -90,9 +91,14 @@ export const SdlBuilder = React.forwardRef<SdlBuilderRefType, Props>(

useEffect(() => {
const { unsubscribe } = watch(data => {
const sdl = generateSdl(data.services as ServiceType[]);
lastSyncedSdlRef.current = sdl;
setEditedManifest(sdl);
try {
const sdl = generateSdl(data as SdlBuilderFormValuesType);
setError(null);
lastSyncedSdlRef.current = sdl;
setEditedManifest(sdl);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate SDL");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
return () => {
unsubscribe();
Expand All @@ -102,41 +108,43 @@ export const SdlBuilder = React.forwardRef<SdlBuilderRefType, Props>(
useEffect(() => {
if (sdlString && sdlString !== lastSyncedSdlRef.current) {
try {
const services = createAndValidateSdl(sdlString);
if (services) {
const imported = createAndValidateSdl(sdlString);
if (imported) {
lastSyncedSdlRef.current = sdlString;
setValue("services", services as ServiceType[]);
form.reset({ ...form.getValues(), placements: imported.placements, services: imported.services });
}
} catch (error) {
setError("Error importing SDL");
}
}
setIsInit(true);
}, [sdlString, setValue]);
}, [sdlString, form]);

useEffect(() => {
onValidate?.({ isValid: formState.isValid });
}, [formState.isValid]);

const getSdl = () => {
try {
return generateSdl(transformCustomSdlFields(formServices, { withSSH: hasComponent("ssh") }));
return generateSdl(transformCustomSdlFields(form.getValues(), { withSSH: hasComponent("ssh") }));
} catch (err) {
if (err instanceof TransformError) {
setError(err.message);
} else {
setError(err instanceof Error ? err.message : String(err));
}
}
};

const createAndValidateSdl = (yamlStr: string) => {
try {
if (!yamlStr) return [];
if (!yamlStr) return null;

const services = importSimpleSdl(yamlStr);
const formValues = importSimpleSdl(yamlStr);

setError(null);

return services;
return formValues;
} catch (err: any) {
if (err.name === "YAMLException" || err.name === "CustomValidationError") {
setError(err.message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { SdlBuilderProvider } from "@src/context/SdlBuilderProvider";
import { useServices } from "@src/context/ServicesProvider";
import { EnvVarManagerService } from "@src/services/remote-deploy/env-var-manager.service";
import { tokens } from "@src/store/remoteDeployStore";
import type { SdlBuilderFormValuesType, ServiceType } from "@src/types";
import { getDefaultService } from "@src/utils/sdl/data";
import type { SdlBuilderFormValuesType } from "@src/types";
import { defaultServiceWithPlacement } from "@src/utils/sdl/data";
import { generateSdl } from "@src/utils/sdl/sdlGenerator";
import { importSimpleSdl } from "@src/utils/sdl/sdlImport";
import BitBranches from "../bitbucket/BitBucketBranches";
Expand All @@ -24,20 +24,26 @@ const RemoteDeployUpdate = ({ sdlString, onManifestChange }: { sdlString: string
const [token] = useAtom(tokens);
const { enqueueSnackbar } = useSnackbar();
const [isEditingEnv, setIsEditingEnv] = useState<number | boolean | null>(false);
const { control, watch, setValue } = useForm<SdlBuilderFormValuesType>({ defaultValues: { services: [getDefaultService()] } });
const initialValues = useMemo(() => defaultServiceWithPlacement(), []);
const { control, watch, setValue } = useForm<SdlBuilderFormValuesType>({
defaultValues: initialValues
});
const { fields: services } = useFieldArray({ control, name: "services", keyName: "id" });
const envVarManagerService = useMemo(() => new EnvVarManagerService(services), [services]);
const { publicConfig } = useServices();

useEffect(() => {
const { unsubscribe }: any = watch(data => {
const sdl = generateSdl(data.services as ServiceType[]);
const sdl = generateSdl(data as SdlBuilderFormValuesType);
onManifestChange(sdl);
});
try {
if (sdlString) {
const services = createAndValidateSdl(sdlString);
setValue("services", services as ServiceType[]);
const imported = createAndValidateSdl(sdlString);
if (imported) {
setValue("placements", imported.placements);
setValue("services", imported.services);
}
}
} catch (error) {
enqueueSnackbar(<Snackbar title="Error while parsing SDL file" />, { variant: "error" });
Expand All @@ -50,7 +56,7 @@ const RemoteDeployUpdate = ({ sdlString, onManifestChange }: { sdlString: string

const createAndValidateSdl = (yamlStr: string) => {
try {
return yamlStr ? importSimpleSdl(yamlStr) : [];
return yamlStr ? importSimpleSdl(yamlStr) : null;
} catch (err: any) {
if (err.name === "YAMLException" || err.name === "CustomValidationError") {
enqueueSnackbar(<Snackbar title={err.message} />, { variant: "error" });
Expand Down
10 changes: 5 additions & 5 deletions apps/deploy-web/src/components/sdl/AttributesFormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { PlacementAttributeType, SdlBuilderFormValuesType } from "@src/type
import { FormPaper } from "./FormPaper";

type Props = {
serviceIndex: number;
placementIndex: number;
control: Control<SdlBuilderFormValuesType, any>;
children?: ReactNode;
attributes: PlacementAttributeType[];
Expand All @@ -22,14 +22,14 @@ export type AttributesRefType = {
_removeAttribute: (index: number | number[]) => void;
};

export const AttributesFormControl = forwardRef<AttributesRefType, Props>(({ control, serviceIndex, attributes: _attributes = [] }, ref) => {
export const AttributesFormControl = forwardRef<AttributesRefType, Props>(({ control, placementIndex, attributes: _attributes = [] }, ref) => {
const {
fields: attributes,
remove: removeAttribute,
append: appendAttribute
} = useFieldArray({
control,
name: `services.${serviceIndex}.placement.attributes`,
name: `placements.${placementIndex}.attributes`,
keyName: "id"
});

Expand Down Expand Up @@ -69,7 +69,7 @@ export const AttributesFormControl = forwardRef<AttributesRefType, Props>(({ con
<div>
<FormField
control={control}
name={`services.${serviceIndex}.placement.attributes.${attIndex}.key`}
name={`placements.${placementIndex}.attributes.${attIndex}.key`}
render={({ field }) => (
<FormInput type="text" label="Key" className="w-full" value={field.value} onChange={event => field.onChange(event.target.value)} />
)}
Expand All @@ -79,7 +79,7 @@ export const AttributesFormControl = forwardRef<AttributesRefType, Props>(({ con
<div className="ml-2">
<FormField
control={control}
name={`services.${serviceIndex}.placement.attributes.${attIndex}.value`}
name={`placements.${placementIndex}.attributes.${attIndex}.value`}
render={({ field }) => (
<FormInput type="text" label="Value" className="w-full" value={field.value} onChange={event => field.onChange(event.target.value)} />
)}
Expand Down
10 changes: 5 additions & 5 deletions apps/deploy-web/src/components/sdl/ImportSdlModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useTheme } from "next-themes";
import { useSnackbar } from "notistack";

import { useServices } from "@src/context/ServicesProvider";
import type { SdlBuilderFormValuesType, ServiceType } from "@src/types";
import type { SdlBuilderFormValuesType } from "@src/types";
import { importSimpleSdl } from "@src/utils/sdl/sdlImport";
import { SDLEditor } from "./SDLEditor/SDLEditor";

Expand All @@ -33,19 +33,18 @@ export const ImportSdlModal: React.FunctionComponent<Props> = ({ onClose, setVal
try {
if (!yamlStr) return null;

const services = importSimpleSdl(yamlStr);
const formValues = importSimpleSdl(yamlStr);

setParsingError(null);

return services;
return formValues;
} catch (err: any) {
if (err.name === "YAMLException" || err.name === "CustomValidationError") {
setParsingError(err.message);
} else if (err.name === "TemplateValidation") {
setParsingError(err.message);
} else {
setParsingError("Error while parsing SDL file");
// setParsingError(err.message);
console.error(err);
}
}
Expand All @@ -56,7 +55,8 @@ export const ImportSdlModal: React.FunctionComponent<Props> = ({ onClose, setVal

if (!result) return;

setValue("services", result as ServiceType[]);
setValue("placements", result.placements);
setValue("services", result.services);

enqueueSnackbar(<Snackbar title="Import success!" iconVariant="success" />, {
variant: "success",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ describe(LogCollectorControl.name, () => {
const logCollectorService = formValues.services.find(service => service.title === `${targetService.title}-log-collector`);
expect(logCollectorService).toBeDefined();
expect(logCollectorService?.image).toMatch(/ghcr\.io\/akash-network\/log-collector:\d+\.\d+\.\d+/);
expect(logCollectorService?.placement).toMatchObject(targetService.placement);
expect(logCollectorService?.placementId).toBe(targetService.placementId);
expect(logCollectorService?.pricing).toMatchObject(targetService.pricing);
});

it("removes log-collector service when checkbox is unchecked", async () => {
Expand Down Expand Up @@ -90,14 +91,36 @@ describe(LogCollectorControl.name, () => {
expect(form.getValues("services.1.title")).toBe(`${targetService.title}-log-collector`);
});

const newPlacement = buildSDLService().placement;
const newPlacementId = "placement-new";
await act(async () => {
form.setValue("services.0.placement", newPlacement);
form.setValue("services.0.placementId", newPlacementId);
});

await vi.waitFor(
() => {
expect(form.getValues("services.1.placement.name")).toBe(newPlacement.name);
expect(form.getValues("services.1.placementId")).toBe(newPlacementId);
},
{ timeout: 1000 }
);
});

it("updates log-collector pricing when target service pricing is changed", async () => {
const { user, form, targetService } = await setup();
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);

await vi.waitFor(() => {
expect(form.getValues("services.1.title")).toBe(`${targetService.title}-log-collector`);
});

const newPricing = { amount: 42_000, denom: "uact" };
await act(async () => {
form.setValue("services.0.pricing", newPricing);
});

await vi.waitFor(
() => {
expect(form.getValues("services.1.pricing")).toEqual(newPricing);
},
{ timeout: 1000 }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,18 @@ export const LogCollectorControl: FC<Props> = ({ serviceIndex, dependencies: d =
}
const nextTitle = toLogCollectorTitle(targetService);

const changes: Partial<Pick<ServiceType, "title" | "placement">> = {};
const changes: Partial<Pick<ServiceType, "title" | "placementId" | "pricing">> = {};

if (logCollectorService.title !== nextTitle) {
changes.title = nextTitle;
}

if (targetService.placement.name !== logCollectorService.placement.name) {
changes.placement = targetService.placement;
if (targetService.placementId !== logCollectorService.placementId) {
changes.placementId = targetService.placementId;
}

if (targetService.pricing.amount !== logCollectorService.pricing.amount || targetService.pricing.denom !== logCollectorService.pricing.denom) {
changes.pricing = targetService.pricing;
}

if (Object.keys(changes).length > 0) {
Expand All @@ -84,7 +88,16 @@ export const LogCollectorControl: FC<Props> = ({ serviceIndex, dependencies: d =
});
}
},
[logCollectorService, logCollectorServiceIndex, targetService.placement.name, targetService.title, update, env]
[
logCollectorService,
logCollectorServiceIndex,
targetService.placementId,
targetService.pricing.amount,
targetService.pricing.denom,
targetService.title,
update,
env
]
);

useThrottledEffect(() => {
Expand Down Expand Up @@ -200,12 +213,15 @@ export function findOwnLogCollectorServiceIndex(service: ServiceType, services:
return services.findIndex(s => s.title === toLogCollectorTitle(service));
}

function generateLogCollectorService<T extends ServiceType>(targetService: T): Pick<T, "placement"> & Omit<ServiceType, "placement"> {
function generateLogCollectorService<T extends ServiceType>(
targetService: T
): Pick<T, "placementId" | "pricing"> & Omit<ServiceType, "placementId" | "pricing"> {
return {
id: toLogCollectorId(targetService),
title: toLogCollectorTitle(targetService),
image: LOG_COLLECTOR_IMAGE,
placement: targetService.placement,
placementId: targetService.placementId,
pricing: targetService.pricing,
env: [
{ key: "PROVIDER", value: "DATADOG" },
{ key: "POD_LABEL_SELECTOR", value: `akash.network/manifest-service=${targetService.title}` },
Expand Down
Loading
Loading