Skip to content
Open
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
2 changes: 1 addition & 1 deletion airflow-core/src/airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@chakra-ui/react": "^3.20.0",
"@codemirror/lang-json": "^6.0.2",
"@emotion/react": "^11.14.0",
"@guanmingchiu/sqlparser-ts": "^0.60.0",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3",
Expand All @@ -52,7 +53,6 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"next-themes": "^0.4.6",
"node-sql-parser": "^5.3.10",
"react": "^19.2.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.2.1",
Expand Down
32 changes: 9 additions & 23 deletions airflow-core/src/airflow/ui/pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ready } from "@guanmingchiu/sqlparser-ts";
import { type ReactNode, useEffect, useState } from "react";

type SqlParserProviderProps = {
readonly children: ReactNode;
readonly fallback?: ReactNode;
};

/**
* Waits for the sqlparser WASM module to load before rendering children.
* This ensures detectLanguage() can detect SQL on the first render.
*/
export const SqlParserProvider = ({ children, fallback }: SqlParserProviderProps) => {
const [isReady, setIsReady] = useState(false);

useEffect(() => {
ready()
.catch(() => {
/* empty */
})
.finally(() => setIsReady(true));
}, []);

if (!isReady) {
return fallback ?? undefined;
}

return children;
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import { Box, Table } from "@chakra-ui/react";
import { useParams } from "react-router-dom";

import { useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
import { SqlParserProvider } from "src/components/SqlParserProvider";
import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
import { useColorMode } from "src/context/colorMode";
import { detectLanguage } from "src/utils/detectLanguage";
import { oneDark, oneLight, SyntaxHighlighter } from "src/utils/syntaxHighlighter";

export const RenderedTemplates = () => {
const RenderedTemplatesContent = () => {
const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams();
const { colorMode } = useColorMode();

Expand Down Expand Up @@ -94,3 +95,9 @@ export const RenderedTemplates = () => {
</Box>
);
};

export const RenderedTemplates = () => (
<SqlParserProvider>
<RenderedTemplatesContent />
</SqlParserProvider>
);
147 changes: 147 additions & 0 deletions airflow-core/src/airflow/ui/src/utils/detectLanguage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* @vitest-environment node
*/
import { ready } from "@guanmingchiu/sqlparser-ts";
import { beforeAll, describe, expect, it } from "vitest";

import { detectLanguage } from "./detectLanguage";

beforeAll(async () => {
await ready();
});

describe("detectLanguage", () => {
describe("JSON detection", () => {
it("detects valid JSON object", () => {
expect(detectLanguage('{"key": "value"}')).toBe("json");
});

it("detects valid JSON array", () => {
expect(detectLanguage("[1, 2, 3]")).toBe("json");
});

it("detects nested JSON", () => {
expect(detectLanguage('{"nested": {"key": "value"}}')).toBe("json");
});
});

describe("SQL detection", () => {
it("detects SELECT statement", () => {
expect(detectLanguage("SELECT * FROM users")).toBe("sql");
});

it("detects SELECT with WHERE clause", () => {
expect(detectLanguage("SELECT id, name FROM users WHERE id = 1")).toBe("sql");
});

it("detects INSERT statement", () => {
expect(detectLanguage("INSERT INTO users (name) VALUES ('test')")).toBe("sql");
});

it("detects UPDATE statement", () => {
expect(detectLanguage("UPDATE users SET name = 'test' WHERE id = 1")).toBe("sql");
});

it("detects DELETE statement", () => {
expect(detectLanguage("DELETE FROM users WHERE id = 1")).toBe("sql");
});

it("detects CREATE TABLE statement", () => {
expect(detectLanguage("CREATE TABLE users (id INT, name VARCHAR(255))")).toBe("sql");
});

it("detects WITH (CTE) statement", () => {
expect(detectLanguage("WITH cte AS (SELECT 1) SELECT * FROM cte")).toBe("sql");
});

it("detects multiline SQL", () => {
const sql = `
SELECT *
FROM users
WHERE id = 1
`;

expect(detectLanguage(sql)).toBe("sql");
});
});

describe("Bash detection", () => {
it("detects shebang", () => {
expect(detectLanguage("#!/bin/bash\necho hello")).toBe("bash");
});

it("detects common bash commands", () => {
expect(detectLanguage("echo 'Hello World'")).toBe("bash");
expect(detectLanguage("ls -la")).toBe("bash");
expect(detectLanguage("cd /tmp")).toBe("bash");
});

it("detects pipe operator", () => {
expect(detectLanguage("cat file.txt | grep pattern")).toBe("bash");
});

it("detects command substitution", () => {
expect(detectLanguage("echo $(date)")).toBe("bash");
});

it("detects logical operators", () => {
expect(detectLanguage("command1 && command2")).toBe("bash");
expect(detectLanguage("command1 || command2")).toBe("bash");
});
});

describe("YAML detection", () => {
it("detects simple YAML", () => {
expect(detectLanguage("key: value")).toBe("yaml");
});

it("detects nested YAML", () => {
const yaml = `
parent:
child: value
`;

expect(detectLanguage(yaml)).toBe("yaml");
});

it("detects YAML list", () => {
const yaml = `
items:
- item1
- item2
`;

expect(detectLanguage(yaml)).toBe("yaml");
});
});

describe("edge cases", () => {
it("handles string with leading/trailing whitespace", () => {
expect(detectLanguage(" SELECT * FROM users ")).toBe("sql");
});

it("prioritizes JSON over YAML for valid JSON", () => {
// Valid JSON is also valid YAML, but JSON should be detected first
expect(detectLanguage('{"key": "value"}')).toBe("json");
});
});
});
20 changes: 4 additions & 16 deletions airflow-core/src/airflow/ui/src/utils/detectLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Parser } from "node-sql-parser";
import { validate } from "@guanmingchiu/sqlparser-ts";
import { parse as parseYaml } from "yaml";

export const detectLanguage = (value: string): string => {
Expand All @@ -31,25 +31,13 @@ export const detectLanguage = (value: string): string => {
// Not valid JSON, continue
}

// Try to detect SQL by parsing with node-sql-parser
// Try to detect SQL using sqlparser-rs
try {
const parser = new Parser();

// Support multiple SQL dialects
parser.astify(trimmed, { database: "postgresql" });
validate(trimmed);

return "sql";
} catch {
// Try with other dialects if PostgreSQL fails
try {
const parser = new Parser();

parser.astify(trimmed, { database: "mysql" });

return "sql";
} catch {
// Not valid SQL, continue to other checks
}
// Not valid SQL, continue to other checks
}

// Try to detect Bash (basic heuristics)
Expand Down
3 changes: 3 additions & 0 deletions airflow-core/src/airflow/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
base: "./",
build: { chunkSizeWarningLimit: 1600, manifest: true },
optimizeDeps: {
exclude: ["@guanmingchiu/sqlparser-ts"], // WASM package needs to be excluded from pre-bundling
},
plugins: [
react({
babel: {
Expand Down
Loading