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

Server side env var interpolation #285

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dd2cd88
feat: envVariables subMenu and editor decorations
Jan 16, 2024
ada9fa3
Merge branch 'main' of github.com:dash0hq/otelbin into server-side-en…
Jan 16, 2024
7a2dfcf
fix: sonarCloud issue
Jan 16, 2024
6b21d06
fix: envVar form style for Safari
Jan 16, 2024
53977a8
fix: envVar data
Jan 17, 2024
a7b7a63
fix: duplicate import
Jan 17, 2024
9b4ebbe
fix: editor decoration unstable, envVars conditions
Jan 18, 2024
22889f0
Merge branch 'main' of github.com:dash0hq/otelbin into server-side-en…
Jan 18, 2024
2167c40
fix: test for extractVariables, set empty string for multiple default…
Jan 19, 2024
2ba4505
fix: sonarCloud bug
Jan 19, 2024
bdb9876
fix: line number changes with different conditions of envVar and defa…
Jan 19, 2024
532dc4a
fix: refactor extractEnvData
Jan 21, 2024
d84fcab
Merge branch 'main' of github.com:dash0hq/otelbin into server-side-en…
Jan 21, 2024
e3cb552
fix: remove commented codes
Jan 21, 2024
2ab1f94
fix: refactor envVarState name, minor improvements
Jan 22, 2024
8c8866d
Merge branch 'main' of github.com:dash0hq/otelbin into server-side-en…
Jan 22, 2024
54e9ebc
fix: lines number and re-render bug
Jan 24, 2024
5b23261
fix: default values
Jan 24, 2024
fa9343c
Merge branch 'main' of github.com:dash0hq/otelbin into server-side-en…
Jan 24, 2024
e5a5f5f
fix: improve editor decorations
Jan 24, 2024
0e94c11
fix: various improvements and fix bugs
Jan 25, 2024
118cfb1
fix: decorations after paste
Jan 25, 2024
a9c4e92
fix: default value on create a new envVar
Jan 25, 2024
3bd85bd
fix: minor fix
Jan 25, 2024
4a86382
fix: improve handling empty values
Jan 25, 2024
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
3 changes: 2 additions & 1 deletion packages/otelbin/.eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
src/lib/urlState/jsurl2.ts
src/lib/urlState/jsurl2.ts
src/components/textArea.tsx
24 changes: 24 additions & 0 deletions packages/otelbin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/otelbin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
Expand Down
158 changes: 158 additions & 0 deletions packages/otelbin/src/components/EnvVarForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: 2023 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useRef, useState } from "react";
import { useEnvVarMenu } from "~/contexts/EditorContext";
import { IconButton } from "./icon-button";
import { Check, X, XCircle } from "lucide-react";
import { Label } from "./label";
import { Textarea } from "./textArea";
import { useUrlState } from "~/lib/urlState/client/useUrlState";
import { envVarBinding } from "./validation/binding";
import React from "react";

export interface IEnvVar {
name: string;
linesNumber: number[];
value: string;
}

export default function EnvVarForm({ envVarData }: { envVarData: Record<string, IEnvVar> }) {
const { openEnvVarMenu, setOpenEnvVarMenu } = useEnvVarMenu();
const [{ env }] = useUrlState([envVarBinding]);

function handleClose() {
setOpenEnvVarMenu(false);
}

const unboundVariables = Object.values(envVarData).filter((envVar) => env[envVar.name] === undefined);

return (
<div
style={{
width: openEnvVarMenu ? `${400}px` : 0,
maxWidth: openEnvVarMenu ? `${400}px` : 0,
transition: "all 0.2s ease-in-out",
}}
className="shrink-0 bg-default shadow-none border-b-default border-r overflow-hidden overflow-y-auto"
>
<div className="w-[400px] flex flex-col h-full">
<div className="flex justify-between items-center px-4 pl-4 pr-1 py-[4.5px] shadow-none border-b-default border-b">
<div className="text-sm text-default">
<span
style={{
color: unboundVariables.length > 0 ? "#F87171" : "#69F18E",
}}
>
{unboundVariables.length}
</span>{" "}
{`${unboundVariables.length > 1 ? "variables" : "variable"} unbound`}
</div>
<IconButton onClick={handleClose} variant={"transparent"} size={"xs"}>
<X height={12} />
</IconButton>
</div>
<div className="px-4">
{Object.values(envVarData).map((envVar) => (
<EnvVar key={envVar.name} envVar={envVar} />
))}
</div>
</div>
</div>
);
}

function EnvVar({ envVar }: { envVar: IEnvVar }) {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [{ env }, getLink] = useUrlState([envVarBinding]);
const [envVarValue, setEnvVarValue] = useState(env[envVar.name] ?? envVar.value);

function handleEnvVarChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setEnvVarValue(event.target.value);
}

function handleEnvVarSubmit() {
if (typeof window !== "undefined") {
window.history.pushState(null, "", getLink({ env: { ...env, [envVar.name]: envVarValue } }));
}
}

useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.style.height = "0px";
const scrollHeight = textAreaRef.current.scrollHeight;
textAreaRef.current.style.height = scrollHeight + "px";
}
}, [envVarValue]);

return (
<div className="flex flex-col gap-y-1 my-6">
<div className="grid w-full items-center gap-1.5 h-full">
<div className="flex gap-x-1 items-center">
<Label htmlFor="envVar">{envVar.name}</Label>
{envVarValue === env[envVar.name] && <Check height={14} color={"#69F18E"} />}
</div>
<div className="relative">
<Textarea
value={envVarValue}
ref={textAreaRef}
onChange={handleEnvVarChange}
className="placeholder:italic h-[35px] min-h-[35px] max-h-[100px] overflow-hidden resize-none w-full pr-10"
id="envVar"
placeholder={env[envVar.name] === "" ? "empty" : "enter value"}
/>
{envVarValue === env[envVar.name] ? (
<IconButton
onClick={() => {
setEnvVarValue("");
}}
variant={"transparent"}
size={"xs"}
className="absolute right-2 top-[6px] z-10"
>
<XCircle height={16} />
</IconButton>
) : envVarValue !== env[envVar.name] ? (
<IconButton
onClick={handleEnvVarSubmit}
variant={"transparent"}
size={"xs"}
className="absolute right-2 top-[6px] z-10"
>
<Check height={16} />
</IconButton>
) : !envVarValue ? (
<IconButton
onClick={handleEnvVarSubmit}
variant={"transparent"}
size={"xs"}
className="absolute right-2 top-[6px] z-10"
>
<Check height={16} />
</IconButton>
) : (
<IconButton
onClick={() => {
setEnvVarValue("");
}}
variant={"transparent"}
size={"xs"}
className="absolute right-2 top-[6px] z-10"
>
<XCircle height={16} />
</IconButton>
)}
</div>
</div>
<Label className="text-[12px] text-[#AFAFB2]" htmlFor="envVar">
{`Used ${envVar.linesNumber.length} ${envVar.linesNumber.length > 1 ? `times` : `time`} on line `}
{envVar.linesNumber.map((lineNumber, index) => (
<React.Fragment key={lineNumber}>
<span className="text-blue-400">{lineNumber}</span>
{index < envVar.linesNumber.length - 1 ? ` and ` : ``}
</React.Fragment>
))}
</Label>
</div>
);
}
22 changes: 22 additions & 0 deletions packages/otelbin/src/components/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

"use client";

import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "~/lib/utils";

const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;

export { Label };
15 changes: 12 additions & 3 deletions packages/otelbin/src/components/monaco-editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import { IconButton } from "~/components/icon-button";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/tooltip";
import { track } from "@vercel/analytics";
import { useServerSideValidation } from "../validation/useServerSideValidation";
import { selectConfigType } from "./parseYaml";
import { extractEnvVarData, extractVariables, selectConfigType } from "./parseYaml";
import EnvVarForm from "../EnvVarForm";
import { envVarBinding } from "../validation/binding";

const firaCode = Fira_Code({
display: "swap",
Expand All @@ -42,10 +44,12 @@ export default function Editor({ locked, setLocked }: { locked: boolean; setLock
const { setViewMode, viewMode } = useViewMode();
const savedOpenModal = Boolean(typeof window !== "undefined" && localStorage.getItem("welcomeModal"));
const [openDialog, setOpenDialog] = useState(savedOpenModal ? !savedOpenModal : true);
const [{ config }, getLink] = useUrlState([editorBinding]);
const [{ config, env }, getLink] = useUrlState([editorBinding, envVarBinding]);
const [currentConfig, setCurrentConfig] = useState<string>(config);
const clerk = useClerk();
const serverSideValidationResult = useServerSideValidation();
const [envVariables, setEnvVariables] = useState<string[]>([]);
const envVarData = extractEnvVarData(envVariables, env, editorRef);
const serverSideValidationResult = useServerSideValidation(envVarData);

const onWidthChange = useCallback((newWidth: number) => {
localStorage.setItem("width", String(newWidth));
Expand Down Expand Up @@ -146,6 +150,10 @@ export default function Editor({ locked, setLocked }: { locked: boolean; setLock
}
}

useEffect(() => {
setEnvVariables(extractVariables(currentConfig));
}, [currentConfig]);

return (
<>
<WelcomeModal open={openDialog} setOpen={setOpenDialog} />
Expand Down Expand Up @@ -191,6 +199,7 @@ export default function Editor({ locked, setLocked }: { locked: boolean; setLock
{viewMode !== "pipeline" && <ValidationErrorConsole errors={totalValidationErrors} font={firaCode} />}
{viewMode == "both" && <ResizeBar onWidthChange={onWidthChange} />}
</div>
{envVariables.length > 0 && <EnvVarForm envVarData={envVarData} />}
<div className="z-0 min-h-full w-full shrink grow relative">
<AutoSizer>
{({ width, height }) => (
Expand Down
31 changes: 31 additions & 0 deletions packages/otelbin/src/components/monaco-editor/parseYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { Parser } from "yaml";
import JsYaml, { FAILSAFE_SCHEMA } from "js-yaml";
import type { editor } from "monaco-editor";
import type { IEnvVar } from "../EnvVarForm";
export interface SourceToken {
type:
| "byte-order-mark"
Expand Down Expand Up @@ -250,3 +252,32 @@ export function selectConfigType(config: string) {
return config;
}
}

export function extractVariables(inputString: string): string[] {
const variableRegex = /\${([^}]+)}/g;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec of the env var detection is here: #156

const matches = inputString.match(variableRegex);

return matches ? matches.map((match) => match) : [];
}

export function extractEnvVarData(
envVars: string[],
envUrlState: Record<string, string>,
editorRef: React.RefObject<editor.IStandaloneCodeEditor | null> | null
) {
let matches: editor.FindMatch[] = [];
const envVarData: Record<string, IEnvVar> = {};

if (envVars && envVars.length > 0) {
envVars.forEach((variable) => {
matches = editorRef?.current?.getModel()?.findMatches(variable, true, false, false, null, false) ?? [];
envVarData[variable] = {
name: variable.slice(2, -1),
linesNumber: matches.map((match) => match.range.startLineNumber),
value: envUrlState[variable.slice(2, -1)] ?? "",
};
});
}

return envVarData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
background-color: #38bdf8 !important;
color: black !important;
}

.envVarDecoration {
color: #fb923c !important;
cursor: pointer;
}
24 changes: 24 additions & 0 deletions packages/otelbin/src/components/textArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2023 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import * as React from "react";

import { cn } from "~/lib/utils";

export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";

export { Textarea };
Loading
Loading