diff --git a/packages/otelbin/.eslintignore b/packages/otelbin/.eslintignore
index 2603e704..4e8caaa5 100644
--- a/packages/otelbin/.eslintignore
+++ b/packages/otelbin/.eslintignore
@@ -1 +1,2 @@
-src/lib/urlState/jsurl2.ts
\ No newline at end of file
+src/lib/urlState/jsurl2.ts
+src/components/textArea.tsx
\ No newline at end of file
diff --git a/packages/otelbin/package-lock.json b/packages/otelbin/package-lock.json
index 8423c7c3..8f17a114 100644
--- a/packages/otelbin/package-lock.json
+++ b/packages/otelbin/package-lock.json
@@ -16,6 +16,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",
@@ -3646,6 +3647,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
+ "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popover": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
diff --git a/packages/otelbin/package.json b/packages/otelbin/package.json
index df12ecb9..7e300eda 100644
--- a/packages/otelbin/package.json
+++ b/packages/otelbin/package.json
@@ -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",
diff --git a/packages/otelbin/src/components/EnvVarForm.tsx b/packages/otelbin/src/components/EnvVarForm.tsx
new file mode 100644
index 00000000..99d34f0d
--- /dev/null
+++ b/packages/otelbin/src/components/EnvVarForm.tsx
@@ -0,0 +1,153 @@
+// SPDX-FileCopyrightText: 2023 Dash0 Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { type IEnvVar, useEnvVarMenu, useEnvLines, type ILine } 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 { editorBinding } from "./monaco-editor/editorBinding";
+import { extractEnvVarData, extractVariables } from "./monaco-editor/parseYaml";
+
+export default function EnvVarForm() {
+ const { openEnvVarMenu, setOpenEnvVarMenu } = useEnvVarMenu();
+ const { envVarLine } = useEnvLines();
+ const [{ env, config }] = useUrlState([editorBinding, envVarBinding]);
+ const variables = useMemo(() => extractVariables(config), [config]);
+ const envVarData = extractEnvVarData(variables, env);
+ const [envVarDataState, setEnvVarDataState] = useState(envVarData);
+ function handleClose() {
+ setOpenEnvVarMenu(false);
+ }
+
+ const unboundVariables = Object.values(envVarDataState).filter(
+ (envVar) => envVar.submittedValue === undefined && envVar.defaultValue === ""
+ );
+
+ useEffect(() => {
+ setEnvVarDataState(extractEnvVarData(variables, env));
+ }, [variables, env]);
+
+ return (
+
+
+
+
+ 0 ? "#F87171" : "#69F18E",
+ }}
+ >
+ {unboundVariables.length}
+ {" "}
+ {`${unboundVariables.length === 1 ? "variable" : "variables"} unbound`}
+
+
+
+
+
+
+ {Object.values(envVarDataState).map((envVar) => (
+
+ ))}
+
+
+
+ );
+}
+
+function EnvVar({ envVar, lines }: { envVar: IEnvVar; lines?: ILine }) {
+ const textAreaRef = useRef(null);
+ const [{ env }, getLink] = useUrlState([envVarBinding, editorBinding]);
+ const [envVarValue, setEnvVarValue] = useState(env[envVar.name] ?? envVar.defaultValue ?? "");
+
+ function handleEnvVarChange(event: React.ChangeEvent) {
+ setEnvVarValue(event.target.value);
+ }
+
+ function handleEnvVarSubmit() {
+ if (typeof window !== "undefined") {
+ window.history.pushState(null, "", getLink({ env: { ...env, [envVar.name]: envVarValue } }));
+ }
+ }
+
+ useEffect(() => {
+ //To enable automatic resizing of the textarea
+ if (textAreaRef.current) {
+ textAreaRef.current.style.height = "0px";
+ const scrollHeight = textAreaRef.current.scrollHeight;
+ textAreaRef.current.style.height = scrollHeight + "px";
+ }
+ }, [envVarValue]);
+
+ useEffect(() => {
+ if (envVar.defaultValue === "") {
+ setEnvVarValue(env[envVar.name] ?? "");
+ } else if (envVar.defaultValue !== "" && envVar.defaultValue !== undefined) {
+ setEnvVarValue(env[envVar.name] ?? envVar.defaultValue ?? "");
+ }
+ }, [env, envVar.defaultValue, envVar.name]);
+
+ return (
+
+
+
+
+ {envVarValue === env[envVar.name] && }
+
+
+
+ {envVarValue === env[envVar.name] ? (
+ {
+ setEnvVarValue("");
+ }}
+ variant={"transparent"}
+ size={"xs"}
+ className="absolute right-2 top-[6px] z-10"
+ >
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {lines && lines.lines.length > 0 && (
+
+ )}
+
+ );
+}
diff --git a/packages/otelbin/src/components/label.tsx b/packages/otelbin/src/components/label.tsx
new file mode 100644
index 00000000..c42f1541
--- /dev/null
+++ b/packages/otelbin/src/components/label.tsx
@@ -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,
+ React.ComponentPropsWithoutRef & VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/packages/otelbin/src/components/monaco-editor/Editor.tsx b/packages/otelbin/src/components/monaco-editor/Editor.tsx
index fca84619..bef35f58 100644
--- a/packages/otelbin/src/components/monaco-editor/Editor.tsx
+++ b/packages/otelbin/src/components/monaco-editor/Editor.tsx
@@ -29,7 +29,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",
@@ -45,10 +47,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 [{ env, config }, getLink] = useUrlState([editorBinding, envVarBinding]);
const [currentConfig, setCurrentConfig] = useState(config);
const clerk = useClerk();
- const serverSideValidationResult = useServerSideValidation();
+ const variables = useMemo(() => extractVariables(config), [config]);
+ const envVarData = extractEnvVarData(variables, env);
+ const serverSideValidationResult = useServerSideValidation(envVarData);
const isServerValidationEnabled = useServerSideValidationEnabled();
const onWidthChange = useCallback((newWidth: number) => {
localStorage.setItem("width", String(newWidth));
@@ -194,6 +198,7 @@ export default function Editor({ locked, setLocked }: { locked: boolean; setLock
{viewMode !== "pipeline" && }
{viewMode == "both" && }
+ {Object.keys(envVarData).length > 0 && }
{({ width, height }) => (
diff --git a/packages/otelbin/src/components/monaco-editor/parseYaml.test.ts b/packages/otelbin/src/components/monaco-editor/parseYaml.test.ts
index 12186acf..f9156723 100644
--- a/packages/otelbin/src/components/monaco-editor/parseYaml.test.ts
+++ b/packages/otelbin/src/components/monaco-editor/parseYaml.test.ts
@@ -3,7 +3,14 @@
import { describe, expect, it } from "@jest/globals";
import type { IItem, IYamlElement } from "./parseYaml";
-import { getYamlDocument, extractServiceItems, findPipelinesKeyValues, parseYaml, selectConfigType } from "./parseYaml";
+import {
+ getYamlDocument,
+ extractServiceItems,
+ findPipelinesKeyValues,
+ parseYaml,
+ selectConfigType,
+ extractVariables,
+} from "./parseYaml";
//The example contains pipelines with duplicated names (otlp and batch)
const editorBinding = {
@@ -245,3 +252,41 @@ describe("selectConfigType", () => {
expect(selectConfigType(config)).toBe(config);
});
});
+
+describe("extractVariables", () => {
+ //The example contains 2 environment variables, one with default value and one without
+ const inputString = {
+ prefix: "",
+ name: "config",
+ fallback: `
+receivers:
+ otlp:
+ endpoint: \${env1:defaultValue1}:14250
+processors:
+ batch:
+ endpoint: \${env2}:14250
+service:
+ extensions: [health_check, pprof, zpages]
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [otlp]
+ metrics:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [otlp]
+`
+ .trim()
+ .replaceAll(/\t/g, " ") as string,
+ } as const;
+
+ it("should extract variables from input string", () => {
+ const result1 = extractVariables(inputString.fallback);
+ expect(result1).toEqual(["${env1:defaultValue1}", "${env2}"]);
+
+ const inputString2 = "No variables in this string";
+ const result2 = extractVariables(inputString2);
+ expect(result2).toEqual([]);
+ });
+});
diff --git a/packages/otelbin/src/components/monaco-editor/parseYaml.ts b/packages/otelbin/src/components/monaco-editor/parseYaml.ts
index ba31eadc..93d8ac10 100644
--- a/packages/otelbin/src/components/monaco-editor/parseYaml.ts
+++ b/packages/otelbin/src/components/monaco-editor/parseYaml.ts
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import YAML, { Parser } from "yaml";
+import type { IEnvVar } from "~/contexts/EditorContext";
export interface SourceToken {
type:
| "byte-order-mark"
@@ -253,3 +254,34 @@ export function selectConfigType(config: string) {
return config;
}
}
+
+export function extractVariables(inputString: string): string[] {
+ const variableRegex = /\${([^}]+)}/g;
+ const matches = inputString.match(variableRegex);
+
+ return matches ? matches.map((match) => match) : [];
+}
+
+export function extractEnvVarData(envVars: string[], envUrlState: Record) {
+ const envVarData: Record = {};
+
+ if (envVars && envVars.length > 0) {
+ const envVarPlaceHolder = envVars.map((variable) => variable.slice(2, -1));
+
+ envVarPlaceHolder.forEach((variable) => {
+ const name = variable.split(":")[0] ?? variable;
+ const defaultValue: string | undefined = variable.split(":")[1] ?? "";
+ const distinctDefaultValues = new Set([...(envVarData[name]?.defaultValues ?? []), defaultValue]);
+ const submittedValue = envUrlState[name];
+
+ envVarData[name] = {
+ name: name,
+ submittedValue: submittedValue,
+ defaultValues: [...distinctDefaultValues],
+ defaultValue: [...distinctDefaultValues].length > 1 ? "" : defaultValue,
+ };
+ });
+ }
+
+ return envVarData;
+}
diff --git a/packages/otelbin/src/components/react-flow/decorationStyles.css b/packages/otelbin/src/components/react-flow/decorationStyles.css
index fdab0d10..8b2cebd6 100644
--- a/packages/otelbin/src/components/react-flow/decorationStyles.css
+++ b/packages/otelbin/src/components/react-flow/decorationStyles.css
@@ -2,3 +2,8 @@
background-color: #38bdf8 !important;
color: black !important;
}
+
+.envVarDecoration {
+ color: #fb923c !important;
+ cursor: pointer;
+}
diff --git a/packages/otelbin/src/components/textArea.tsx b/packages/otelbin/src/components/textArea.tsx
new file mode 100644
index 00000000..8c3703f2
--- /dev/null
+++ b/packages/otelbin/src/components/textArea.tsx
@@ -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 {}
+
+const Textarea = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/packages/otelbin/src/components/validation-type/ValidationType.tsx b/packages/otelbin/src/components/validation-type/ValidationType.tsx
index f5989eb3..0aa7c763 100644
--- a/packages/otelbin/src/components/validation-type/ValidationType.tsx
+++ b/packages/otelbin/src/components/validation-type/ValidationType.tsx
@@ -1,15 +1,18 @@
// SPDX-FileCopyrightText: 2023 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { Button } from "../button";
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
import Down from "./../assets/svg/down.svg";
import ValidationTypeContent from "./ValidationTypeContent";
import { useDistributions } from "../validation/useDistributions";
import { useUrlState } from "~/lib/urlState/client/useUrlState";
-import { distroBinding, distroVersionBinding } from "../validation/binding";
+import { distroBinding, distroVersionBinding, envVarBinding } from "../validation/binding";
import InfoBox from "./InfoBox";
+import { editorBinding } from "../monaco-editor/editorBinding";
+import WarningBox from "./WarningBox";
+import { extractEnvVarData, extractVariables } from "../monaco-editor/parseYaml";
export interface ICurrentDistributionVersion {
distro: string;
@@ -18,9 +21,13 @@ export interface ICurrentDistributionVersion {
}
export default function ValidationType() {
- const [{ distro, distroVersion }] = useUrlState([distroBinding, distroVersionBinding]);
+ const [{ distro, distroVersion, config, env }] = useUrlState([
+ distroBinding,
+ distroVersionBinding,
+ envVarBinding,
+ editorBinding,
+ ]);
const [open, setOpen] = useState(false);
-
const { data: distributions } = useDistributions();
const currentDistributionVersion =
@@ -28,6 +35,13 @@ export default function ValidationType() {
? { distro: distro, version: distroVersion, name: distributions[distro]?.name || "" }
: undefined;
+ const variables = useMemo(() => extractVariables(config), [config]);
+ const envVarData = extractEnvVarData(variables, env);
+
+ const unboundVariables = Object.values(envVarData).filter(
+ (envVar) => envVar.submittedValue === undefined && envVar.defaultValue === ""
+ );
+
return (
@@ -49,6 +63,9 @@ export default function ValidationType() {
{distro === null && distroVersion === null &&
}
+ {distro !== null && distroVersion !== null && unboundVariables.length > 0 && (
+
+ )}
);
}
diff --git a/packages/otelbin/src/components/validation-type/WarningBox.tsx b/packages/otelbin/src/components/validation-type/WarningBox.tsx
new file mode 100644
index 00000000..6398a8e7
--- /dev/null
+++ b/packages/otelbin/src/components/validation-type/WarningBox.tsx
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 Dash0 Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { AlertTriangle } from "lucide-react";
+
+export default function WarningBox({ unboundVariables }: { unboundVariables: number }) {
+ return (
+
+
+
{`Validation disabled: ${unboundVariables} environment variable is unbound`}
+
+ );
+}
diff --git a/packages/otelbin/src/components/validation/useServerSideValidation.ts b/packages/otelbin/src/components/validation/useServerSideValidation.ts
index 4dbf98b8..6e29c1bd 100644
--- a/packages/otelbin/src/components/validation/useServerSideValidation.ts
+++ b/packages/otelbin/src/components/validation/useServerSideValidation.ts
@@ -9,6 +9,7 @@ import { distroBinding, distroVersionBinding, envVarBinding } from "~/components
import { editorBinding } from "~/components/monaco-editor/editorBinding";
import { useEffect, useMemo, useState } from "react";
import { type ServerSideValidationResult } from "~/types";
+import type { IEnvVar } from "~/contexts/EditorContext";
export interface ValidationState {
isLoading: boolean;
@@ -29,7 +30,7 @@ const initialValidationState: ValidationState = {
result: null,
};
-export function useServerSideValidation(): ValidationState {
+export function useServerSideValidation(envVarData: Record): ValidationState {
const [{ config, distro, distroVersion, env }] = useUrlState([
distroBinding,
distroVersionBinding,
@@ -37,12 +38,14 @@ export function useServerSideValidation(): ValidationState {
envVarBinding,
]);
const [state, setState] = useState(initialValidationState);
-
+ const unboundVariables = Object.values(envVarData).filter(
+ (envVar) => envVar.submittedValue === undefined && envVar.defaultValue === ""
+ );
const validate = useMemo(
() =>
debounce(
async (config: string) => {
- if (!distro || !distroVersion) {
+ if (!distro || !distroVersion || unboundVariables.length > 0) {
return;
}
@@ -98,7 +101,7 @@ export function useServerSideValidation(): ValidationState {
trailing: true,
}
),
- [distro, distroVersion, env]
+ [distro, distroVersion, env, unboundVariables.length]
);
useEffect(() => {
diff --git a/packages/otelbin/src/contexts/EditorContext.tsx b/packages/otelbin/src/contexts/EditorContext.tsx
index f70bd7f7..d34cd180 100644
--- a/packages/otelbin/src/contexts/EditorContext.tsx
+++ b/packages/otelbin/src/contexts/EditorContext.tsx
@@ -1,15 +1,16 @@
// SPDX-FileCopyrightText: 2023 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0
-import React, { createContext, useEffect, useRef, useState } from "react";
+import React, { createContext, useEffect, useMemo, useRef, useState } from "react";
import type { ReactNode, RefObject } from "react";
-import type { editor } from "monaco-editor";
+import { type editor, type IPosition } from "monaco-editor";
import { type Monaco, type OnMount } from "@monaco-editor/react";
import { configureMonacoYaml, type MonacoYamlOptions } from "monaco-yaml";
import schema from "../components/monaco-editor/schema.json";
import { fromPosition, toCompletionList } from "monaco-languageserver-types";
import { type languages } from "monaco-editor/esm/vs/editor/editor.api.js";
-import { type IItem, getYamlDocument, selectConfigType } from "../components/monaco-editor/parseYaml";
+import "../components/react-flow/decorationStyles.css";
+import { type IItem, getYamlDocument, selectConfigType, extractVariables } from "../components/monaco-editor/parseYaml";
import { type WorkerGetter, createWorkerManager } from "monaco-worker-manager";
import { type CompletionList, type Position } from "vscode-languageserver-types";
import {
@@ -17,6 +18,18 @@ import {
validateOtelCollectorConfigurationAndSetMarkers,
} from "~/components/monaco-editor/otelCollectorConfigValidation";
+export interface ILine {
+ lines: number[];
+}
+
+export interface IEnvVar {
+ name: string;
+ submittedValue?: string;
+ defaultValues?: string[];
+ defaultValue?: string;
+ lines?: ILine;
+}
+
interface YAMLWorker {
doComplete: (uri: string, position: Position) => CompletionList | undefined;
}
@@ -60,6 +73,26 @@ export const BreadcrumbsContext = createContext<{
},
});
+export const EnvVarMenuContext = createContext<{
+ openEnvVarMenu: boolean;
+ setOpenEnvVarMenu: (openEnvVarMenu: boolean) => void;
+}>({
+ openEnvVarMenu: false,
+ setOpenEnvVarMenu: () => {
+ return;
+ },
+});
+
+export const EnvVarLine = createContext<{
+ envVarLine: Record;
+ setEnvVarLine: (envVarLine: Record) => void;
+}>({
+ envVarLine: {},
+ setEnvVarLine: () => {
+ return;
+ },
+});
+
export function useEditorRef() {
return React.useContext(EditorContext);
}
@@ -84,15 +117,28 @@ export function useBreadcrumbs() {
return React.useContext(BreadcrumbsContext);
}
+export function useEnvVarMenu() {
+ return React.useContext(EnvVarMenuContext);
+}
+
+export function useEnvLines() {
+ return React.useContext(EnvVarLine);
+}
+
export const EditorProvider = ({ children }: { children: ReactNode }) => {
const editorRef = useRef(null);
+ const [editorRefState, setEditorRefState] = useState();
const monacoRef = useRef(null);
const [focused, setFocused] = useState("");
const [viewMode, setViewMode] = useState("both");
const [path, setPath] = useState("");
const isServerValidationEnabled = useServerSideValidationEnabled();
- const viewState = editorRef.current?.saveViewState();
const [monaco, setMonaco] = useState();
+ const [openEnvVarMenu, setOpenEnvVarMenu] = useState(true);
+ const currentValue = editorRefState?.getModel()?.getValue() ?? "";
+ const variables = useMemo(() => extractVariables(currentValue), [currentValue]);
+ const [envVarLine, setEnvVarLine] = useState>({});
+ const [oldDecorations, setOldDecorations] = useState([]);
useEffect(() => {
if (!isServerValidationEnabled && monaco) {
@@ -117,8 +163,75 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
}
}, [isServerValidationEnabled, monaco]);
+ useEffect(() => {
+ envVarDecoration(variables);
+ setEnvVarLine(extractLineNumbers(variables));
+ }, [variables]);
+
+ function extractLineNumbers(envVars: string[]) {
+ const envVarLines: Record = {};
+
+ if (envVars && envVars.length > 0 && editorRefState) {
+ const envVarPlaceHolder = envVars.map((variable) => variable.slice(2, -1));
+
+ envVarPlaceHolder.forEach((variable) => {
+ const name = variable.split(":")[0] ?? variable;
+ const matchCondition = name === variable ? "${" + name + "}" : "${" + name + ":";
+ const matches =
+ editorRef?.current?.getModel()?.findMatches(matchCondition, true, false, false, null, false) ?? [];
+
+ envVarLines[name] = {
+ lines: matches.map((match) => match.range.startLineNumber),
+ };
+ });
+ }
+
+ return envVarLines;
+ }
+
+ function envVarDecoration(variables: string[]) {
+ if (variables.length === 0) return;
+ const decorations: editor.IModelDeltaDecoration[] = [];
+ let matches: editor.FindMatch[] = [];
+
+ if (editorRefState && monaco) {
+ variables.forEach((variable) => {
+ matches = editorRefState.getModel()?.findMatches(variable, true, false, false, null, false) ?? [];
+ if (matches?.length > 0) {
+ matches.forEach((match) => {
+ const range = match.range;
+ const startLineNumber = range.startLineNumber;
+ const startColumn = range.startColumn;
+ const endLineNumber = range.endLineNumber;
+ const endColumn = range.endColumn;
+ const decoration = {
+ range: {
+ startLineNumber: startLineNumber,
+ startColumn: startColumn,
+ endLineNumber: endLineNumber,
+ endColumn: endColumn,
+ },
+ options: {
+ isWholeLine: false,
+ inlineClassName: "envVarDecoration",
+ stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
+ },
+ };
+ decorations.push(decoration);
+ });
+ }
+ });
+ const newDecoration = editorRefState.getModel()?.deltaDecorations(oldDecorations, decorations);
+ if (newDecoration) {
+ setOldDecorations(newDecoration);
+ }
+ }
+ }
+
function editorDidMount(editor: editor.IStandaloneCodeEditor, monaco: Monaco) {
editorRef.current = editor;
+ monacoRef.current = monaco;
+ setEditorRefState(editor);
setMonaco(monaco);
window.MonacoEnvironment = {
@@ -134,9 +247,6 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
},
};
- monacoRef.current = monaco;
- editorRef.current?.restoreViewState(viewState as editor.ICodeEditorViewState);
-
validateOtelCollectorConfigurationAndSetMarkers(
editorRef.current.getModel()?.getValue() || "",
editorRef,
@@ -145,7 +255,7 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
);
monaco.languages.setLanguageConfiguration("yaml", {
- wordPattern: /\w+\/[\w_]+(?:-[\w_]+)*|\w+/,
+ wordPattern: /\${([^}]+:[^}]+)}|\${([^}]+)}|(?:\w+\/[\w_]+(?:-[\w_]+)*|\w+)/,
});
const worker = createWorkerManager(monaco, {
@@ -187,9 +297,11 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
let value = editorRef.current?.getValue() ?? "";
let docElements = getYamlDocument(value);
+
editorRef.current?.onDidChangeModelContent(() => {
value = editorRef.current?.getValue() ?? "";
docElements = getYamlDocument(value);
+ envVarDecoration(variables);
});
function correctKey(value: string, key?: string, key2?: string) {
@@ -250,6 +362,22 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
}
}
+ editorRef.current.onMouseDown(() => {
+ const envVarRegex = /\${([^}]+)}/g;
+ const cursorOffset = editorRef?.current?.getPosition() as IPosition;
+ const wordAtCursor: editor.IWordAtPosition = editorRef?.current?.getModel()?.getWordAtPosition(cursorOffset) || {
+ word: "",
+ startColumn: 0,
+ endColumn: 0,
+ };
+
+ if (wordAtCursor.word.match(envVarRegex)) {
+ setOpenEnvVarMenu(true);
+ }
+ });
+
+ envVarDecoration(variables);
+
editorRef.current.onDidChangeCursorPosition((e) => {
const cursorOffset = editorRef?.current?.getModel()?.getOffsetAt(e.position) ?? 0;
const wordAtCursor: editor.IWordAtPosition = editorRef?.current?.getModel()?.getWordAtPosition(e.position) || {
@@ -284,13 +412,27 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
path: path,
};
+ const envVarMenuContext = {
+ openEnvVarMenu: openEnvVarMenu,
+ setOpenEnvVarMenu: setOpenEnvVarMenu,
+ };
+
+ const envVarLineContext = {
+ envVarLine: envVarLine,
+ setEnvVarLine: setEnvVarLine,
+ };
+
return (
- {children}
+
+
+ {children}
+
+
diff --git a/packages/otelbin/src/lib/urlState/client/useUrlState.ts b/packages/otelbin/src/lib/urlState/client/useUrlState.ts
index cd512c5a..2f515a8d 100644
--- a/packages/otelbin/src/lib/urlState/client/useUrlState.ts
+++ b/packages/otelbin/src/lib/urlState/client/useUrlState.ts
@@ -28,7 +28,7 @@ export function useUrlState[]>(
const searchParams = useSearchParams();
const hashSearchParams = useHashSearchParams();
- const urlState = useMemo(() => parseUrlState(hashSearchParams, binds), [hashSearchParams, binds]);
+ const urlState = useMemo(() => parseUrlState(hashSearchParams, binds), [hashSearchParams, ...binds]);
const getLink = useCallback(
function getLink(newUrlState: Partial>, newPathName?: string): string {