Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.
Merged
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
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

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

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ reqwest = "0.11.22"
tokio = { version = "1.33.0", features = ["full"] }
base64 = "0.21.6"
window-shadows = "0.2.2"
regex = "1.10.2"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
Expand Down
84 changes: 84 additions & 0 deletions src-tauri/src/app_handle/format_console_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::collections::HashMap;
use regex::Regex;

#[tauri::command]
pub fn format_console_text(
console_text: Vec<String>,
chunk_size: usize,
style_dict: HashMap<String, Vec<String>>,
) -> String {
// Define a regex pattern to find URLs
let url_pattern = Regex::new(r"https?://\S+").unwrap();

// Split the console text into chunks of size chunk_size and format each chunk.
let mut chunks: Vec<String> = Vec::new();
let mut chunk: String = String::new();

for line in console_text {
if chunk.len() >= chunk_size {
if let Ok(formatted_chunk) = format_chunk(&chunk, &style_dict, &url_pattern) {
chunks.push(formatted_chunk);
} else {
chunks.push(chunk.clone()); // Use unformatted chunk if formatting fails
}
chunk = String::new();
}

if !chunk.is_empty() {
chunk.push('\n');
}

chunk.push_str(&line);
}

if !chunk.is_empty() {
if let Ok(formatted_chunk) = format_chunk(&chunk, &style_dict, &url_pattern) {
chunks.push(formatted_chunk);
} else {
chunks.push(chunk.clone()); // Use unformatted chunk if formatting fails
}
}

chunks.join("\n")
}

// If the chunk contains a URL, wrap it in HTML <a> tags with href.
fn format_chunk(
chunk: &str,
style_dict: &HashMap<String, Vec<String>>,
url_pattern: &Regex,
) -> Result<String, regex::Error> {
// Function to replace URLs with HTML <a> tags
fn replace_url_with_link(captures: &regex::Captures) -> String {
let url = captures.get(0).unwrap().as_str();
format!("<a href=\"{}\">{}</a>", url, url)
}

// Apply any additional formatting based on the style_dict
let mut formatted_chunk = chunk.to_owned();

for (key, styles) in style_dict {
let pattern = format!(r"(\b{}\b)", regex::escape(key));
let regex = regex::Regex::new(&pattern)?;

formatted_chunk = regex
.replace_all(&formatted_chunk, |captures: &regex::Captures| {
let match_str = captures.get(0).unwrap().as_str();
apply_styled_text(match_str.to_string(), styles.clone())
})
.to_string();
}

// Replace URLs with HTML <a> tags
formatted_chunk = url_pattern
.replace_all(&formatted_chunk, replace_url_with_link)
.to_string();

Ok(formatted_chunk)
}

// Function to wrap text in <span> tags with class names for styles/colors
fn apply_styled_text(text: String, styles: Vec<String>) -> String {
let class_names = styles.join(" ");
format!("<span class=\"{}\">{}</span>", class_names, text)
}
4 changes: 3 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod jenkins {

mod app_handle {
pub mod jenkins_calls;
pub mod format_console_text;
}

#[tokio::main]
Expand All @@ -24,7 +25,8 @@ async fn main() {
app_handle::jenkins_calls::start_build_with_parameters,
app_handle::jenkins_calls::start_build,
app_handle::jenkins_calls::get_test_result_data,
app_handle::jenkins_calls::get_jenkins_data
app_handle::jenkins_calls::get_jenkins_data,
app_handle::format_console_text::format_console_text,
])
.setup(|app| {
let main_window = app.get_window("main").unwrap();
Expand Down
8 changes: 4 additions & 4 deletions src/Icons/pack_1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,16 +308,16 @@ export const IcoMaximize = ({ size = 24, color = "currentColor", className }: Ic
// Used in: Feature Buttons
export const IcoMinimize = ({ size = 24, color = "currentColor", className }: IconProps) => (
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M6 12L18 12" stroke={color} stroke-width="2" stroke-linecap="round" />
<path d="M6 12L18 12" stroke={color} strokeWidth="2" strokeLinecap="round" />
</svg>
)
);

// Icon Pack: Jarvis
// License: MIT - Copyright (c) 2024 JNSAPH
// Used in: Feature Buttons
export const IcoClose = ({ size = 24, color = "currentColor", className }: IconProps) => (
<svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M7.41418 7L17.3137 16.8995" stroke={color} stroke-width="2" stroke-linecap="round" />
<path d="M16.8995 7L6.99998 16.8995" stroke={color} stroke-width="2" stroke-linecap="round" />
<path d="M7.41418 7L17.3137 16.8995" stroke={color} strokeWidth="2" strokeLinecap="round" />
<path d="M16.8995 7L6.99998 16.8995" stroke={color} strokeWidth="2" strokeLinecap="round" />
</svg>
);
17 changes: 17 additions & 0 deletions src/components/ConsoleViewRenderHTML/ConsoleViewRenderHTML.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";

// react component that takes in a string and returns that string as rendered html
interface Props {
htmlString: string;
}

const ConsoleViewRenderHTML: React.FC<Props> = ({ htmlString }) => {
return /*#__PURE__*/React.createElement("pre", {
dangerouslySetInnerHTML: {
__html: htmlString
},
className: "overflow-auto px-6 py-4 console-custom-scroll"
});
};

export default ConsoleViewRenderHTML;
2 changes: 1 addition & 1 deletion src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ export const JARVIS_LOADING_MESSAGES = [
"Reviewing the etiquette rulebook",
];

export const CONSOLE_VIEW_CHUNK_SIZE = 50;
export const CONSOLE_VIEW_CHUNK_SIZE = 10000;
37 changes: 16 additions & 21 deletions src/screens/Jarvis/Views/ConsoleView/ConsoleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import { IJenkinsBuild } from "../../../../Interfaces/IBuildInterface";
import StorageManager from "../../../../helpers/StorageManager";
import ConsoleViewLoading from "./ConsoleViewLoading";
import { fetchUtils } from "../../Utils/fetchUtils";
import { CONSOLE_RELOAD_TIME, CONSOLE_VIEW_CHUNK_SIZE } from "../../../../config/constants";
import { CONSOLE_RELOAD_TIME } from "../../../../config/constants";

import { motion } from "framer-motion";
import { WebviewWindow } from "@tauri-apps/api/window";
import { formatConsoleData } from "./ConsoleViewUtils";
import { generateRandomString } from "../../../../helpers/utils";
import { clearIntervalId, setIntervalId } from "./IntervalManager";
import { IcoArrowDown, IcoDownload, IcoFile, IcoLinear } from "@/Icons/pack_1";
import { formatConsoleData } from "./ConsoleViewUtils";
import ConsoleViewRenderHTML from "@/components/ConsoleViewRenderHTML/ConsoleViewRenderHTML";

interface Props {
buildData: IJenkinsBuild;
}
const ConsoleView: React.FC<Props> = ({ buildData }) => {
const consoleRef = useRef<HTMLDivElement | null>(null);
const preElementRef = useRef<HTMLPreElement | null>(null);
const [consoleData, setConsoleData] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(true);
const [autoScroll, setAutoScroll] = useState<boolean>(false);
Expand All @@ -31,26 +31,25 @@ const ConsoleView: React.FC<Props> = ({ buildData }) => {

const lines = await fetchUtils.consoleText(projectName, buildNumber);

const chunks = Array.from({ length: Math.ceil(lines.length / CONSOLE_VIEW_CHUNK_SIZE) }, (_, index) =>
lines.slice(index * CONSOLE_VIEW_CHUNK_SIZE, (index + 1) * CONSOLE_VIEW_CHUNK_SIZE)
);

const formattedChunks = await Promise.all(
chunks.map((chunk) => formatConsoleData(chunk))
);

const formattedData = formattedChunks.join("\n");
const formattedData = await formatConsoleData(lines);

// add the new data to the console data
setConsoleData(formattedData);
setIsLoading(false);

if (buildData.result !== null) {
clearIntervalId()
Logger.info("ConsoleView/ConsoleView.tsx", "ConsoleView: Build is finished, clearing interval")
clearIntervalId();
Logger.info(
"ConsoleView/ConsoleView.tsx",
"ConsoleView: Build is finished, clearing interval"
);
return true;
}
} catch (error) {
Logger.error("ConsoleView/ConsoleView.tsx", "ConsoleView: Error while fetching console data: " + error);
Logger.error(
"ConsoleView/ConsoleView.tsx",
"ConsoleView: Error while fetching console data: " + error
);
}
};

Expand Down Expand Up @@ -102,7 +101,7 @@ const ConsoleView: React.FC<Props> = ({ buildData }) => {
setIntervalId(setInterval(() => {
fetchAndSetConsoleData();
}, CONSOLE_RELOAD_TIME));
}
};

startConsole();

Expand Down Expand Up @@ -140,11 +139,7 @@ const ConsoleView: React.FC<Props> = ({ buildData }) => {
{/* <p className="font-medium text-sm cursor-pointer">Full Output</p> */}
</div>
<div>
<pre
className="overflow-auto px-6 py-4 console-custom-scroll"
ref={preElementRef} // Use preElementRef as the ref for the <pre> element
dangerouslySetInnerHTML={{ __html: consoleData }}
/>
<ConsoleViewRenderHTML htmlString={consoleData} />
</div>
</div>
{buildData.result === null && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const ConsoleViewLoading: React.FC = () => {
return (
<div className="bg-console-background border-2 border-border rounded-md shadow-lg px-6 py-5 overflow-auto">
<div className="animate-pulse">

<div className="w-1/2 text-comment-color overflow-hidden h-3 mt-2 bg-comment-color rounded-full opacity-40" />
<div className="w-1/3 text-comment-color overflow-hidden h-3 mt-2 bg-comment-color rounded-full opacity-40" />
<div className="w-1/4 text-comment-color overflow-hidden h-3 mt-2 bg-comment-color rounded-full opacity-40" />
Expand Down
26 changes: 0 additions & 26 deletions src/screens/Jarvis/Views/ConsoleView/ConsoleViewStyleDict.ts

This file was deleted.

85 changes: 32 additions & 53 deletions src/screens/Jarvis/Views/ConsoleView/ConsoleViewUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,39 @@
import Logger from "@/helpers/Logger";
import { CONSOLE_VIEW_CHUNK_SIZE } from "@/config/constants";
import { invoke } from "@tauri-apps/api";
import {
exists, BaseDirectory, createDir, writeTextFile, readTextFile,
} from "@tauri-apps/api/fs";
import { stylingDict as defaultStylingDict } from "./styleDict";
import { CONSOLE_VIEW_STYLE_FILE } from "../../../../config/constants";
import Logger from "../../../../helpers/Logger";
import { IStylingDict } from "../../../../Interfaces/StylingDict";
import { getConsoleViewStyleDict } from "./ConsoleViewStyleDict";
import { type } from "@tauri-apps/api/os";

let osType: string;

(async () => {
osType = await type();
Logger.info("ConsoleView/ConsoleViewUtils.tsx", `OS Type: ${osType}`);
})();


const handleApplyStyledDataWebWorker = (data: string, stylingDict: IStylingDict): Promise<string> => {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL("./worker", import.meta.url), { type: "module" });

worker.onmessage = (e) => {
resolve(e.data);
worker.terminate();
};

worker.onerror = (e) => {
reject(e);
worker.terminate();
};

worker.postMessage({ data, stylingDict });
export const formatConsoleData = async (lines: string[]): Promise<string> => {
const formattedData: string = await invoke("format_console_text", {
consoleText: lines,
chunkSize: CONSOLE_VIEW_CHUNK_SIZE,
styleDict: await getConsoleViewStyleDict()
});
};

export const formatConsoleData = async (lines: string[]) => {
// Process each line to replace URLs with clickable links
const formattedLines = lines.map((line) => line.replace(
/https?:\/\/[^\s]+/g,
(match) => `<a href="${match}" target="_blank">${match}</a>`,
));

// Join the formatted lines back together with newline characters
const formattedData = formattedLines.join("\n");

// If Browser is Safari return the formatted data
if (osType === "Darwin") {
return formattedData;
}
return formattedData;
};

const stylingDict: IStylingDict = await getConsoleViewStyleDict();
// if stylingDict is empty return the formatted data
if (!stylingDict || Object.keys(stylingDict).length === 0) {
console.log("stylingDict is empty");

return formattedData;
export async function getConsoleViewStyleDict(): Promise<IStylingDict> {
try {
const appDataDirExists = await exists("", { dir: BaseDirectory.AppData });

if (!appDataDirExists) {
await createDir("", { dir: BaseDirectory.AppData });
}

if (await exists(CONSOLE_VIEW_STYLE_FILE, { dir: BaseDirectory.AppData })) {
return await JSON.parse(await readTextFile(CONSOLE_VIEW_STYLE_FILE, { dir: BaseDirectory.AppData }));
}
await writeTextFile(CONSOLE_VIEW_STYLE_FILE, await JSON.stringify(defaultStylingDict), { dir: BaseDirectory.AppData });
return defaultStylingDict;
} catch (error) {
Logger.error("ConsoleView/ConsoleViewStyleDict.tsx", `Error getting ${CONSOLE_VIEW_STYLE_FILE}. The File might be empty. \n${error}`);
return defaultStylingDict;
}


// Apply styles based on the dictionary and get the styled data
const styledData = await handleApplyStyledDataWebWorker(formattedData, stylingDict);

return styledData;
};
}
Loading