diff --git a/web-admin/src/features/projects/environment-variables/ActionsCell.svelte b/web-admin/src/features/projects/environment-variables/ActionsCell.svelte index 2a2cf6b5ddb..c875613b19f 100644 --- a/web-admin/src/features/projects/environment-variables/ActionsCell.svelte +++ b/web-admin/src/features/projects/environment-variables/ActionsCell.svelte @@ -24,7 +24,7 @@ - + { diff --git a/web-admin/src/features/projects/environment-variables/AddDialog.svelte b/web-admin/src/features/projects/environment-variables/AddDialog.svelte index ce5692c7466..c413a2268ae 100644 --- a/web-admin/src/features/projects/environment-variables/AddDialog.svelte +++ b/web-admin/src/features/projects/environment-variables/AddDialog.svelte @@ -27,6 +27,7 @@ import IconButton from "@rilldata/web-common/components/button/IconButton.svelte"; import { Trash2Icon, UploadIcon } from "lucide-svelte"; import { getCurrentEnvironment, isDuplicateKey } from "./utils"; + import { parse as parseDotenv } from "dotenv"; export let open = false; export let variableNames: VariableNames = []; @@ -36,15 +37,16 @@ let isDevelopment = true; let isProduction = true; let fileInput: HTMLInputElement; + let showEnvironmentError = false; $: organization = $page.params.organization; $: project = $page.params.project; - $: isEnvironmentSelected = isDevelopment || isProduction; $: hasExistingKeys = Object.values(inputErrors).some((error) => error); $: hasNewChanges = $form.variables.some( (variable) => variable.key !== "" || variable.value !== "", ); + $: hasNoEnvironment = showEnvironmentError && !isDevelopment && !isProduction; const queryClient = useQueryClient(); const updateProjectVariables = createAdminServiceUpdateProjectVariables(); @@ -72,11 +74,11 @@ }), ); - const { form, enhance, submit, submitting, errors, allErrors, reset } = - superForm(defaults(initialValues, schema), { + const { form, enhance, submit, submitting, errors, allErrors } = superForm( + defaults(initialValues, schema), + { SPA: true, validators: schema, - // See: https://superforms.rocks/concepts/nested-data dataType: "json", async onUpdate({ form }) { if (!form.valid) return; @@ -85,11 +87,9 @@ // Check for duplicates before proceeding const duplicates = checkForExistingKeys(); if (duplicates > 0) { - // Early return without resetting the form return; } - // Only filter and process if there are no duplicates const filteredVariables = values.variables.filter( ({ key }) => key !== "", ); @@ -106,7 +106,8 @@ console.error(error); } }, - }); + }, + ); async function handleUpdateProjectVariables( flatVariables: AdminServiceUpdateProjectVariablesBodyVariables, @@ -144,6 +145,8 @@ function handleKeyChange(index: number, event: Event) { const target = event.target as HTMLInputElement; $form.variables[index].key = target.value; + delete inputErrors[index]; + isKeyAlreadyExists = false; } function handleValueChange(index: number, event: Event) { @@ -157,31 +160,37 @@ } function handleReset() { - reset(); + $form = initialValues; isDevelopment = true; isProduction = true; inputErrors = {}; isKeyAlreadyExists = false; + showEnvironmentError = false; } function checkForExistingKeys() { inputErrors = {}; isKeyAlreadyExists = false; - const existingKeys = $form.variables.map((variable) => { - return { - environment: getCurrentEnvironment(isDevelopment, isProduction), - name: variable.key, - }; - }); + const existingKeys = $form.variables + .filter((variable) => variable.key.trim() !== "") + .map((variable) => { + return { + environment: getCurrentEnvironment(isDevelopment, isProduction), + name: variable.key, + }; + }); let duplicateCount = 0; - existingKeys.forEach((key, idx) => { + existingKeys.forEach((key, _idx) => { const variableEnvironment = key.environment; const variableKey = key.name; if (isDuplicateKey(variableEnvironment, variableKey, variableNames)) { - inputErrors[idx] = true; + const originalIndex = $form.variables.findIndex( + (v) => v.key === variableKey, + ); + inputErrors[originalIndex] = true; isKeyAlreadyExists = true; duplicateCount++; } @@ -190,55 +199,62 @@ return duplicateCount; } - function handleFileUpload(event) { - const file = event.target.files[0]; + function handleFileUpload(event: Event) { + const file = (event.target as HTMLInputElement).files?.[0]; if (file) { const reader = new FileReader(); - reader.onload = (e) => { - const contents = e.target.result; - parseFile(contents); - checkForExistingKeys(); + reader.onload = (e: ProgressEvent) => { + const contents = e.target?.result; + if (typeof contents === "string") { + parseFile(contents); + checkForExistingKeys(); + } }; reader.readAsText(file); } } - function parseFile(contents) { - const lines = contents.split("\n"); + function parseFile(contents: string) { + const parsedVariables = parseDotenv(contents); - lines.forEach((line) => { - // Trim the line and check if it starts with '#' - const trimmedLine = line.trim(); - if (trimmedLine.startsWith("#")) { - return; // Skip comment lines - } + for (const [key, value] of Object.entries(parsedVariables)) { + const filteredVariables = $form.variables.filter( + (variable) => + variable.key.trim() !== "" || variable.value.trim() !== "", + ); - const [key, value] = trimmedLine.split("="); - if (key && value) { - if (key.trim() && value.trim()) { - const filteredVariables = $form.variables.filter( - (variable) => - variable.key.trim() !== "" || variable.value.trim() !== "", - ); - - $form.variables = [ - ...filteredVariables, - { key: key.trim(), value: value.trim() }, - ]; - } - } - }); + $form.variables = [...filteredVariables, { key, value }]; + } } function getKeyFromError(error: { path: string; messages: string[] }) { return error.path.split("[")[1].split("]")[0]; } + + function handleEnvironmentChange() { + showEnvironmentError = true; + checkForExistingKeys(); + } + + $: isSubmitDisabled = + $submitting || + hasExistingKeys || + !hasNewChanges || + hasNoEnvironment || + Object.values($form.variables).every((v) => !v.key.trim()); handleReset()} - onOutsideClick={() => handleReset()} + onOpenChange={(isOpen) => { + if (!isOpen) { + handleReset(); + } + }} + onOutsideClick={() => { + open = false; + handleReset(); + }} > @@ -279,18 +295,25 @@
Environment
+ {#if hasNoEnvironment} +
+

+ You must select at least one environment +

+
+ {/if}
Variables
@@ -378,17 +401,18 @@ on:click={() => { open = false; handleReset(); - }}>Cancel + Cancel + + Create +
diff --git a/web-admin/src/features/projects/environment-variables/EditDialog.svelte b/web-admin/src/features/projects/environment-variables/EditDialog.svelte index f6c1f662021..50ccf108f1a 100644 --- a/web-admin/src/features/projects/environment-variables/EditDialog.svelte +++ b/web-admin/src/features/projects/environment-variables/EditDialog.svelte @@ -23,11 +23,9 @@ import { object, string } from "yup"; import { EnvironmentType, type VariableNames } from "./types"; import Input from "@rilldata/web-common/components/forms/Input.svelte"; - import { - getCurrentEnvironment, - getEnvironmentType, - isDuplicateKey, - } from "./utils"; + import { getCurrentEnvironment, isDuplicateKey } from "./utils"; + import { onMount } from "svelte"; + import { debounce } from "lodash"; export let open = false; export let id: string; @@ -40,21 +38,22 @@ isDevelopment: boolean; isProduction: boolean; }; - let isDevelopment: boolean; - let isProduction: boolean; + let isDevelopment = false; + let isProduction = false; let isKeyAlreadyExists = false; let inputErrors: { [key: number]: boolean } = {}; + let showEnvironmentError = false; $: organization = $page.params.organization; $: project = $page.params.project; - $: isEnvironmentSelected = isDevelopment || isProduction; $: hasNewChanges = $form.key !== initialValues.key || $form.value !== initialValues.value || initialEnvironment?.isDevelopment !== isDevelopment || initialEnvironment?.isProduction !== isProduction; $: hasExistingKeys = Object.values(inputErrors).some((error) => error); + $: hasNoEnvironment = showEnvironmentError && !isDevelopment && !isProduction; const queryClient = useQueryClient(); const updateProjectVariables = createAdminServiceUpdateProjectVariables(); @@ -79,40 +78,32 @@ }), ); - const { - form, - enhance, - formId, - submit, - errors, - allErrors, - submitting, - reset, - } = superForm(defaults(initialValues, schema), { - // See: https://superforms.rocks/concepts/multiple-forms#setting-id-on-the-client - id: id, - SPA: true, - validators: schema, - async onUpdate({ form }) { - if (!form.valid) return; - const values = form.data; - - checkForExistingKeys(); - if (isKeyAlreadyExists) return; - - const flatVariable = { - [values.key]: values.value, - }; - - try { - await handleUpdateProjectVariables(flatVariable); - open = false; - handleReset(); - } catch (error) { - console.error(error); - } - }, - }); + const { form, enhance, formId, submit, errors, allErrors, submitting } = + superForm(defaults(initialValues, schema), { + id: id, + SPA: true, + validators: schema, + resetForm: false, + async onUpdate({ form }) { + if (!form.valid) return; + const values = form.data; + + checkForExistingKeys(); + if (isKeyAlreadyExists) return; + + const flatVariable = { + [values.key]: values.value, + }; + + try { + await handleUpdateProjectVariables(flatVariable); + open = false; + handleReset(); + } catch (error) { + console.error(error); + } + }, + }); async function handleUpdateProjectVariables( flatVariable: AdminServiceUpdateProjectVariablesBodyVariables, @@ -200,28 +191,30 @@ } function handleReset() { - reset(); + $form.environment = initialValues.environment; + $form.key = initialValues.key; + $form.value = initialValues.value; isDevelopment = false; isProduction = false; inputErrors = {}; isKeyAlreadyExists = false; + showEnvironmentError = false; } function checkForExistingKeys() { inputErrors = {}; isKeyAlreadyExists = false; - const existingKey = { - environment: getEnvironmentType( - getCurrentEnvironment(isDevelopment, isProduction), - ), - name: $form.key, - }; - - const variableEnvironment = existingKey.environment; - const variableKey = existingKey.name; + const newEnvironment = getCurrentEnvironment(isDevelopment, isProduction); - if (isDuplicateKey(variableEnvironment, variableKey, variableNames)) { + if ( + isDuplicateKey( + newEnvironment, + $form.key, + variableNames, + initialValues.key, + ) + ) { inputErrors[0] = true; isKeyAlreadyExists = true; } @@ -240,19 +233,37 @@ isDevelopment = true; isProduction = true; } - } - function handleDialogOpen() { - setInitialCheckboxState(); initialEnvironment = { isDevelopment, isProduction, }; } + function handleDialogOpen() { + handleReset(); + setInitialCheckboxState(); + } + + function handleEnvironmentChange() { + showEnvironmentError = true; + } + + onMount(() => { + handleDialogOpen(); + }); + $: if (open) { handleDialogOpen(); } + + const debouncedCheckForExistingKeys = debounce(() => { + checkForExistingKeys(); + }, 500); + + $: if ($form.key) { + debouncedCheckForExistingKeys(); + } + {#if hasNoEnvironment} +
+

+ You must select at least one environment +

+
+ {/if}
Variable
@@ -349,11 +369,12 @@ form={$formId} disabled={$submitting || !hasNewChanges || - !isEnvironmentSelected || hasExistingKeys || - $allErrors.length > 0} - submitForm>Edit + Edit +
diff --git a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte index e896e6d9222..34bfbcb1996 100644 --- a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte +++ b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte @@ -42,7 +42,6 @@ { header: "Activity", accessorFn: (row) => row.createdOn, - enableSorting: false, cell: ({ row }) => { return flexRender(ActivityCell, { updatedOn: row.original.updatedOn, diff --git a/web-admin/src/features/projects/environment-variables/ValueCell.svelte b/web-admin/src/features/projects/environment-variables/ValueCell.svelte index b37f8cf0284..3c9a1267e4a 100644 --- a/web-admin/src/features/projects/environment-variables/ValueCell.svelte +++ b/web-admin/src/features/projects/environment-variables/ValueCell.svelte @@ -1,8 +1,10 @@ + diff --git a/web-common/src/components/icons/EyeInvisible.svelte b/web-common/src/components/icons/EyeInvisible.svelte index 649cfb79c61..fe3f60f59ad 100644 --- a/web-common/src/components/icons/EyeInvisible.svelte +++ b/web-common/src/components/icons/EyeInvisible.svelte @@ -1,16 +1,21 @@ + + diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index 664606bbe23..9c7056df558 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -81,7 +81,7 @@ style={`margin-left: ${marginLeft};`} class:cursor-pointer={header.column.getCanSort()} class:select-none={header.column.getCanSort()} - class="font-semibold text-gray-500 flex flex-row items-center gap-x-1 truncate" + class="font-semibold text-gray-500 flex flex-row items-center gap-x-1 truncate text-sm" >