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

FEAT: Support Markdown Rendering #871

Merged
merged 26 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c016753
Add markdown rendering support to WF and PS log components
tahierhussain Nov 22, 2024
11f3fa9
Add markdown rendering support to API Deployment logs component
tahierhussain Nov 22, 2024
69c0277
New component to render markdown only for bold, link and new line
tahierhussain Nov 24, 2024
86cde0a
Use the new CustomMarkdown component for rendering logs and notificat…
tahierhussain Nov 24, 2024
067ee26
Optimize the CustomMarkdown component
tahierhussain Nov 25, 2024
8fd1e56
Support selective markdown formatting for RJSF form's helper text
tahierhussain Nov 25, 2024
e0c7c29
Merge branch 'main' of github.com:Zipstack/unstract into feat/support…
tahierhussain Nov 26, 2024
5f76b97
Merge branch 'main' of github.com:Zipstack/unstract into feat/support…
tahierhussain Dec 4, 2024
ae677d1
Add code block support for triple backticks and refine UI elements
tahierhussain Dec 4, 2024
9013814
Add form-level description and reduce spacing between checkbox fields…
tahierhussain Dec 4, 2024
c38be7b
Minor bug fixes in rendering RJSF form
tahierhussain Dec 4, 2024
18af199
Optimize the RjsfLayout component
tahierhussain Dec 4, 2024
763f2ff
Merge branch 'main' into feat/support-markdown-rendering
tahierhussain Dec 5, 2024
70c8ee0
Merge branch 'main' of github.com:Zipstack/unstract into feat/support…
tahierhussain Dec 9, 2024
3150dcb
Merge branch 'feat/support-markdown-rendering' of github.com:Zipstack…
tahierhussain Dec 9, 2024
c8d6db0
Refactor CustomMarkdown to use regex for efficient and streamlined ma…
tahierhussain Dec 9, 2024
2ed6935
Add optional chaining to improve null/undefined safety wherever neces…
tahierhussain Dec 9, 2024
49d078e
Fix sonar issue w.r.t regex string
tahierhussain Dec 9, 2024
b8a9cf6
Split regex into separate variables to avoid SonarQube false-positive…
tahierhussain Dec 9, 2024
f2095e9
Add maximum repetition limits to regex to prevent super-linear runtime
tahierhussain Dec 9, 2024
5ed7ba9
Simplify regex in CustomMarkdown to reduce complexity and address Son…
tahierhussain Dec 9, 2024
484e5dc
Revert "Simplify regex in CustomMarkdown to reduce complexity and add…
tahierhussain Dec 9, 2024
4e21d44
Merge branch 'main' into feat/support-markdown-rendering
tahierhussain Dec 10, 2024
d7df173
Merge branch 'main' into feat/support-markdown-rendering
gaya3-zipstack Dec 11, 2024
b9b2e34
Simplify and optimize CustomMarkdown component with bounded regex pat…
tahierhussain Dec 11, 2024
620ee4b
Merge branch 'main' into feat/support-markdown-rendering
gaya3-zipstack Dec 11, 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 frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAlertStore } from "./store/alert-store.js";
import { useSessionStore } from "./store/session-store.js";
import PostHogPageviewTracker from "./PostHogPageviewTracker.js";
import { useEffect } from "react";
import CustomMarkdown from "./components/helpers/custom-markdown/CustomMarkdown.jsx";

let GoogleTagManagerHelper;
try {
Expand Down Expand Up @@ -46,7 +47,7 @@ function App() {

notificationAPI.open({
message: alertDetails?.title,
description: alertDetails?.content,
description: <CustomMarkdown text={alertDetails?.content} />,
type: alertDetails?.type,
duration: alertDetails?.duration,
btn,
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/agency/display-logs/DisplayLogs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Col, Row, Typography } from "antd";

import "./DisplayLogs.css";
import { useSocketLogsStore } from "../../../store/socket-logs-store";
import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown";

function DisplayLogs() {
const bottomRef = useRef(null);
Expand Down Expand Up @@ -37,9 +38,10 @@ function DisplayLogs() {
</Typography>
</Col>
<Col span={8}>
<Typography className="display-logs-col">
{log?.message}
</Typography>
<CustomMarkdown
text={log?.message}
styleClassName="display-logs-col"
/>
</Col>
<Col span={2}>
<Typography className="display-logs-col">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Col, Row, Typography } from "antd";
import "../../agency/display-logs/DisplayLogs.css";
import { useSocketCustomToolStore } from "../../../store/socket-custom-tool";
import { getDateTimeString } from "../../../helpers/GetStaticData";
import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown";

function DisplayLogs() {
const bottomRef = useRef(null);
Expand Down Expand Up @@ -48,9 +49,10 @@ function DisplayLogs() {
</Typography>
</Col>
<Col span={8}>
<Typography className="display-logs-col">
{message?.message}
</Typography>
<CustomMarkdown
text={message?.message}
styleClassName="display-logs-col"
/>
</Col>
</Row>
<div ref={bottomRef} />
Expand Down
112 changes: 112 additions & 0 deletions frontend/src/components/helpers/custom-markdown/CustomMarkdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useMemo } from "react";
import { Typography } from "antd";
import PropTypes from "prop-types";

const { Text, Link, Paragraph } = Typography;

const CustomMarkdown = ({
text = "",
renderNewLines = true,
isSecondary = false,
styleClassName,
}) => {
const textType = isSecondary ? "secondary" : undefined;

/*
Patterns with bounded quantifiers to prevent performance issues:
- Triple code: ```...```
- Inline code: `...`
- Bold: **...**
- Link: [text](url)
- New line: \n
*/
const patterns = [
{ type: "tripleCode", regex: /```([^`]{1,1000})```/g },
{ type: "inlineCode", regex: /`([^`]{1,100})`/g },
{ type: "bold", regex: /\*\*([^*]{1,100})\*\*/g },
{ type: "link", regex: /\[([^\]]{1,100})\]\(([^)]{1,200})\)/g },
{ type: "newline", regex: /\n/g },
];

const renderToken = (type, content, url) => {
switch (type) {
case "tripleCode":
return (
<Paragraph style={{ margin: 0 }}>
<pre style={{ margin: 0 }}>{content}</pre>
</Paragraph>
);
case "inlineCode":
return (
<Text code type={textType}>
{content}
</Text>
);
case "bold":
return (
<Text strong type={textType}>
{content}
</Text>
);
case "link":
return (
<Link href={url} target="_blank" rel="noopener noreferrer">
{content}
</Link>
);
case "newline":
return renderNewLines ? <br /> : "\n";
default:
return content;
}
};

const content = useMemo(() => {
let elements = [text];

// Process each pattern sequentially
patterns.forEach(({ type, regex }) => {
const newElements = [];
elements.forEach((element) => {
if (typeof element !== "string") {
newElements.push(element);
return;
}

let lastIndex = 0;
let match;
while ((match = regex.exec(element)) !== null) {
const matchIndex = match.index;
if (matchIndex > lastIndex) {
newElements.push(element.substring(lastIndex, matchIndex));
}
const url = match[2]; // Relevant only for link
newElements.push(renderToken(type, match[1], url));
lastIndex = matchIndex + match[0].length;
}

if (lastIndex < element.length) {
newElements.push(element.substring(lastIndex));
}
});
elements = newElements;
});

return elements;
}, [text, renderNewLines, textType]);

return (
<Text type={textType} className={styleClassName}>
{content}
</Text>
);
};

CustomMarkdown.propTypes = {
text: PropTypes.string,
renderNewLines: PropTypes.bool,
isSecondary: PropTypes.bool,
styleClassName: PropTypes.string,
};

export default CustomMarkdown;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate.js";
import { useAlertStore } from "../../../store/alert-store.js";
import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
import "./LogsModel.css";
import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown.jsx";

const LogsModal = ({
open,
Expand Down Expand Up @@ -38,16 +39,16 @@ const LogsModal = ({
};
axiosPrivate(requestOptions)
.then((res) => {
const logDetails = res.data.results.map((item) => ({
id: item.id,
log: item.data?.log,
type: item.data?.type,
stage: item.data?.stage,
level: item.data?.level,
event_time: item.event_time,
const logDetails = res?.data?.results?.map((item) => ({
id: item?.id,
log: <CustomMarkdown text={item?.data?.log} />,
type: item?.data?.type,
stage: item?.data?.stage,
level: item?.data?.level,
event_time: item?.event_time,
}));
setLogDetails(logDetails);
setTotalCount(res.data.count);
setTotalCount(res?.data?.count);
})
.catch((err) => {
setAlertDetails(handleException(err));
Expand All @@ -74,7 +75,7 @@ const LogsModal = ({
return (
<Button
type="link"
onClick={() => handleLogIdClick(record.execution_id)}
onClick={() => handleLogIdClick(record?.execution_id)}
>
{text}
</Button>
Expand All @@ -85,7 +86,7 @@ const LogsModal = ({
title: "Status",
dataIndex: "status",
key: "status",
render: (level) => <span className={level.toLowerCase()}>{level}</span>,
render: (level) => <span className={level?.toLowerCase()}>{level}</span>,
},
];

Expand All @@ -104,14 +105,15 @@ const LogsModal = ({
title: "Log Level",
dataIndex: "level",
key: "level",
render: (level) => <span className={level.toLowerCase()}>{level}</span>,
render: (level) => <span className={level?.toLowerCase()}>{level}</span>,
},
{
title: "Log",
dataIndex: "log",
key: "log",
},
];

return (
<>
<Modal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import PropTypes from "prop-types";

import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx";

const AltDateTimeWidget = ({ id, value, onChange, label, required }) => {
const AltDateTimeWidget = ({
id,
value,
onChange,
label,
schema,
required,
}) => {
const description = schema?.description || "";
const handleDateChange = (date) => {
onChange(date?.toISOString());
};
Expand All @@ -17,7 +25,11 @@ const AltDateTimeWidget = ({ id, value, onChange, label, required }) => {
};

return (
<RjsfWidgetLayout label={label} required={required}>
<RjsfWidgetLayout
label={label}
description={description}
required={required}
>
<DatePicker
id={id}
value={value ? moment(value) : null}
Expand All @@ -36,6 +48,7 @@ AltDateTimeWidget.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
required: PropTypes.bool,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import PropTypes from "prop-types";

import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx";

const AltDateWidget = ({ id, value, onChange, label, required }) => {
const AltDateWidget = ({ id, value, onChange, label, schema, required }) => {
const description = schema?.description || "";

const handleDateChange = (date) => {
onChange(date?.toISOString());
};

return (
<RjsfWidgetLayout label={label} required={required}>
<RjsfWidgetLayout
label={label}
description={description}
required={required}
>
<DatePicker
id={id}
value={value ? moment(value) : null}
Expand All @@ -25,6 +31,7 @@ AltDateWidget.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
required: PropTypes.bool,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { QuestionCircleOutlined } from "@ant-design/icons";
import { Checkbox, Space, Tooltip, Typography } from "antd";
import { Checkbox, Space, Typography } from "antd";
import PropTypes from "prop-types";
import "./CheckboxWidget.css";
const CheckboxWidget = ({ id, value, onChange, label, schema }) => {
import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown";
const CheckboxWidget = ({ id, value, onChange, label, schema, required }) => {
const description = schema?.description || "";
const handleCheckboxChange = (event) => {
onChange(event.target.checked);
};

return (
<Space className="checkbox-widget-main">
<Space direction="vertical" className="checkbox-widget-main" size={0}>
<Checkbox id={id} checked={value} onChange={handleCheckboxChange}>
<Typography>{label}</Typography>
<Typography>
{required && <span className="form-item-required">* </span>}
{label}
vishnuszipstack marked this conversation as resolved.
Show resolved Hide resolved
</Typography>
</Checkbox>
{description?.length > 0 && (
<Tooltip title={description}>
<QuestionCircleOutlined className="checkbox-widget-info-icon" />
</Tooltip>
<CustomMarkdown
text={description}
isSecondary={true}
styleClassName="rjsf-helper-font"
/>
)}
</Space>
);
Expand All @@ -28,6 +33,7 @@ CheckboxWidget.propTypes = {
onChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
required: PropTypes.bool,
};

export { CheckboxWidget };
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { Checkbox } from "antd";

import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx";

const CheckboxesWidget = ({ id, options, value, onChange, label }) => {
const CheckboxesWidget = ({
id,
options,
value,
onChange,
label,
schema,
required,
}) => {
const description = schema?.description || "";
const handleCheckboxChange = (optionValue) => {
const newValue = [...(value || [])];
const index = newValue.indexOf(optionValue);
Expand All @@ -16,7 +25,11 @@ const CheckboxesWidget = ({ id, options, value, onChange, label }) => {
};

return (
<RjsfWidgetLayout label={label}>
<RjsfWidgetLayout
label={label}
description={description}
required={required}
>
{options.map((option) => (
<Checkbox
key={option.value}
Expand All @@ -36,6 +49,8 @@ CheckboxesWidget.propTypes = {
value: PropTypes.array,
onChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
required: PropTypes.bool,
};

export { CheckboxesWidget };
Loading
Loading