Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Env Var Feedback #6223

Merged
merged 24 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
122 changes: 99 additions & 23 deletions web-admin/src/features/projects/environment-variables/AddDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -190,43 +190,119 @@
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<FileReader>) => {
const contents = e.target?.result;
if (typeof contents === "string") {
parseFile(contents);
checkForExistingKeys();
}
};
reader.readAsText(file);
}
}

function parseFile(contents) {
const lines = contents.split("\n");
// TEST CASES:
// Basic:
// KEY=value -> { key: "KEY", value: "value" }
// KEY= -> { key: "KEY", value: "" }
//
// Quoted strings:
// STRING="hello world" -> { key: "STRING", value: "hello world" }
// STRING='hello world' -> { key: "STRING", value: "hello world" }
// STRING="hello 'world'" -> { key: "STRING", value: "hello 'world'" }
// STRING='hello "world"' -> { key: "STRING", value: 'hello "world"' }
//
// Escaped quotes:
// JSON="{\"key\": \"value\"}" -> { key: "JSON", value: '{"key": "value"}' }
// QUOTE="String with \"quotes\"" -> { key: "QUOTE", value: 'String with "quotes"' }
//
// Special cases:
// EMPTY="" -> { key: "EMPTY", value: "" }
// EMPTY_SINGLE='' -> { key: "EMPTY_SINGLE", value: "" }
// EQUALS="key=value" -> { key: "EQUALS", value: "key=value" }
// MULTILINE="first\nsecond" -> { key: "MULTILINE", value: "first\nsecond" }
// SPACES=" trim me " -> { key: "SPACES", value: " trim me " }
// UNICODE="🚀" -> { key: "UNICODE", value: "🚀" }
function parseFile(contents: string) {
lovincyrus marked this conversation as resolved.
Show resolved Hide resolved
const lines = contents.split(/\r?\n/); // Handle both CRLF and LF line endings

lines.forEach((line) => {
// Trim the line and check if it starts with '#'
const trimmedLine = line.trim();
if (trimmedLine.startsWith("#")) {
return; // Skip comment lines

// Skip empty lines, comments, and export statements
if (
!trimmedLine ||
trimmedLine.startsWith("#") ||
trimmedLine.startsWith("export ")
) {
return;
}

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() },
];
// Find the first non-escaped equals sign
let splitIndex = -1;
let inQuotes = false;
let quoteChar = "";

for (let i = 0; i < trimmedLine.length; i++) {
const char = trimmedLine[i];

if ((char === '"' || char === "'") && trimmedLine[i - 1] !== "\\") {
if (!inQuotes) {
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar) {
inQuotes = false;
}
}

if (char === "=" && !inQuotes) {
splitIndex = i;
break;
}
}

if (splitIndex === -1) return; // No valid equals sign found

const key = trimmedLine.slice(0, splitIndex).trim();
let value = trimmedLine.slice(splitIndex + 1).trim();

if (!key) return;

// Handle quoted values
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
try {
// Use JSON.parse for double-quoted strings to handle escapes properly
if (value.startsWith('"')) {
value = JSON.parse(value);
} else {
// For single quotes, manually handle basic escapes
value = value
.slice(1, -1)
.replace(/\\'/g, "'")
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\\\/g, "\\");
}
} catch (error) {
console.warn(`Failed to parse quoted value for key "${key}":`, error);
// Keep the original value if parsing fails
}
}

const filteredVariables = $form.variables.filter(
(variable) =>
variable.key.trim() !== "" || variable.value.trim() !== "",
);

$form.variables = [...filteredVariables, { key, value }];
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
import Input from "@rilldata/web-common/components/forms/Input.svelte";
import {
getCurrentEnvironment,
getEnvironmentType,

Check failure on line 28 in web-admin/src/features/projects/environment-variables/EditDialog.svelte

View workflow job for this annotation

GitHub Actions / build

'getEnvironmentType' is defined but never used
isDuplicateKey,
} from "./utils";
import { onMount } from "svelte";

export let open = false;
export let id: string;
Expand All @@ -40,21 +41,21 @@
isDevelopment: boolean;
isProduction: boolean;
};
let isDevelopment: boolean;
let isProduction: boolean;
let isDevelopment = false;
let isProduction = false;
let isKeyAlreadyExists = false;
let inputErrors: { [key: number]: boolean } = {};

$: 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 = !isDevelopment && !isProduction;

const queryClient = useQueryClient();
const updateProjectVariables = createAdminServiceUpdateProjectVariables();
Expand Down Expand Up @@ -211,17 +212,16 @@
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;
}
Expand All @@ -240,16 +240,21 @@
isDevelopment = true;
isProduction = true;
}
}

function handleDialogOpen() {
setInitialCheckboxState();
initialEnvironment = {
isDevelopment,
isProduction,
};
}

function handleDialogOpen() {
setInitialCheckboxState();
}

onMount(() => {
handleDialogOpen();
});

$: if (open) {
handleDialogOpen();
}
Expand Down Expand Up @@ -294,6 +299,13 @@
label="Production"
/>
</div>
{#if hasNoEnvironment}
<div class="mt-1">
<p class="text-xs text-red-600 font-normal">
You must select at least one environment
</p>
</div>
{/if}
</div>
<div class="flex flex-col items-start gap-1">
<div class="text-sm font-medium text-gray-800">Variable</div>
Expand Down Expand Up @@ -349,11 +361,12 @@
form={$formId}
disabled={$submitting ||
!hasNewChanges ||
!isEnvironmentSelected ||
hasExistingKeys ||
$allErrors.length > 0}
submitForm>Edit</Button
hasNoEnvironment}
submitForm
>
Edit
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
{
header: "Activity",
accessorFn: (row) => row.createdOn,
enableSorting: false,
cell: ({ row }) => {
return flexRender(ActivityCell, {
updatedOn: row.original.updatedOn,
Expand Down
32 changes: 14 additions & 18 deletions web-admin/src/features/projects/environment-variables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,24 @@ export function getEnvironmentType(environment: string) {
* @returns true if the key would create a duplicate, false otherwise
*/
export function isDuplicateKey(
environment: string,
environment: EnvironmentType,
key: string,
variableNames: VariableNames,
currentKey?: string,
): boolean {
return variableNames.some((variable) => {
// Only consider it a duplicate if the same key exists
if (variable.name === key) {
// If either the existing or new variable is for all environments, it's a duplicate
if (
variable.environment === EnvironmentType.UNDEFINED ||
environment === EnvironmentType.UNDEFINED
) {
return true;
}
// If the key is the same as the current key, it's not a duplicate
if (currentKey && key === currentKey) return false;

// If trying to create in the same environment as existing variable
if (variable.environment === environment) {
return true;
}
}
return false;
});
const hasMatchingKey = (variable) => variable.name === key;
const isInTargetEnvironment = (variable) =>
environment === EnvironmentType.UNDEFINED ||
variable.environment === environment ||
variable.environment === EnvironmentType.UNDEFINED;

// Check if the key already exists in the target environment or all environments
return variableNames.some(
(variable) => hasMatchingKey(variable) && isInTargetEnvironment(variable),
);
}

export function getCurrentEnvironment(
Expand Down
Loading