Skip to content

Commit

Permalink
Merge pull request #1019 from ga-devfront/feat/progress-tracker-script
Browse files Browse the repository at this point in the history
[NEW UI] progress tracker script
  • Loading branch information
Quetzacoalt91 authored Nov 22, 2024
2 parents e8c7610 + 25f5c1d commit a1ee13e
Show file tree
Hide file tree
Showing 31 changed files with 878 additions and 298 deletions.
56 changes: 53 additions & 3 deletions _dev/src/scss/components/_logs.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
@use "../variables" as *;

$e: ".logs";
$e-tracker: ".progress-tracker";
$e-logs: ".logs";

#{$ua-id} {
#{$e} {
#{$e-tracker} {
display: flex;
flex-direction: column;
height: 100%;
}

#{$e-logs} {
--#{$ua-prefix}logs-height: 14rem;
--#{$ua-prefix}logs-background-color: var(--#{$ua-prefix}muted-background-color);
display: flex;
Expand All @@ -29,10 +36,33 @@ $e: ".logs";
}

&__scroll {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: var(--#{$ua-prefix}logs-height);

&::before {
content: "";
position: absolute;
top: 0;
display: block;
width: 100%;
height: 1rem;
background-image: linear-gradient(
to top,
transparent,
var(--#{$ua-prefix}muted-background-color)
);
}
}

&__scroll-inner {
height: 100%;
padding-block-start: 1rem;
overflow-y: auto;
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
}

&__list {
Expand All @@ -44,35 +74,55 @@ $e: ".logs";

&__line {
padding-inline-start: 2rem;
background-color: rgb(255 255 255 / 0);
background-repeat: no-repeat;
background-position: left center;
background-size: 1.5rem 1.5rem;
font-size: 0.875rem;
line-height: 1.4;
word-break: break-word;
transition: background-color 0.3s ease-in-out;
scroll-snap-align: start;
scroll-margin-block-start: 2.5rem;

&--success {
background-image: url("../../img/check.svg");

&#{$e-logs}__line--pointed {
color: var(--#{$ua-prefix}green-500);
}
}

&--warning {
background-image: url("../../img/warning.svg");

&#{$e-logs}__line--pointed {
color: var(--#{$ua-prefix}yellow-500);
}
}

&--error {
background-image: url("../../img/close.svg");

&#{$e-logs}__line--pointed {
color: var(--#{$ua-prefix}red-500);
}
}
}

&__summary-anchor {
margin-inline-start: 0.25rem;
font-weight: 700;
}

&__summaries {
display: flex;
flex-direction: column;
gap: 1rem;
padding-block-end: 0.5rem;

&:empty {
display: none;
}
}

&__summary {
Expand Down
19 changes: 19 additions & 0 deletions _dev/src/ts/components/ComponentAbstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default abstract class ComponentAbstract {
readonly #element: HTMLElement;

public constructor(element: HTMLElement) {
this.#element = element;
}

public get element(): HTMLElement {
return this.#element;
}

protected queryElement = <T extends HTMLElement>(selector: string, errorMessage: string): T => {
const element = (this.element.querySelector(selector) as T) ?? document.querySelector(selector);
if (!element) {
throw new Error(errorMessage);
}
return element;
};
}
17 changes: 17 additions & 0 deletions _dev/src/ts/components/LogsSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ComponentAbstract from './ComponentAbstract';

export default class LogsSummary extends ComponentAbstract {
#logsSummaryText = this.queryElement<HTMLDivElement>(
'[data-slot-component="text"]',
'Logs summary text not found'
);

/**
* @public
* @param text - text summary to display.
* @description Allows to update the summary text of the logs.
*/
public setLogsSummaryText = (text: string): void => {
this.#logsSummaryText.innerText = text;
};
}
233 changes: 233 additions & 0 deletions _dev/src/ts/components/LogsViewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import ComponentAbstract from './ComponentAbstract';
import { SeverityClasses, LogEntry } from '../types/logsTypes';
import { parseLogWithSeverity } from '../utils/logsUtils';

export default class LogsViewer extends ComponentAbstract {
#warnings: string[] = [];
#errors: string[] = [];
#isSummaryDisplayed: boolean = false;

#logsList = this.queryElement<HTMLDivElement>(
'[data-slot-component="list"]',
'Logs list not found'
);
#logsScroll = this.queryElement<HTMLDivElement>(
'[data-slot-component="scroll"]',
'Logs scroll not found'
);
#logsSummary = this.queryElement<HTMLDivElement>(
'[data-slot-component="summary"]',
'Logs summary not found'
);
#templateLogLine = this.queryElement<HTMLTemplateElement>(
'#log-line',
'Template log line not found'
);
#templateSummary = this.queryElement<HTMLTemplateElement>(
'#log-summary',
'Template summary not found'
);

/**
* @public
* @param {string[]} logs - Array of log strings to be added.
* @description Adds logs to the viewer and updates the DOM with log lines.
* Logs with specific severity (WARNING, ERROR) are tracked with unique IDs.
* Prevents adding logs if the summary is already displayed.
*/
public addLogs = (logs: string[]): void => {
if (this.#isSummaryDisplayed) {
console.warn('Cannot add logs while the summary is displayed');
return;
}

const fragment = document.createDocumentFragment();

logs.forEach((log) => {
const logEntry = parseLogWithSeverity(log);
const logLine = this.#createLogLine(logEntry);

if (logEntry.className === SeverityClasses.WARNING) {
const id = `warning-${this.#warnings.length}`;
this.#warnings.push(id);
logLine.id = id;
}

if (logEntry.className === SeverityClasses.ERROR) {
const id = `error-${this.#errors.length}`;
this.#errors.push(id);
logLine.id = id;
}

fragment.appendChild(logLine);
});

this.#appendFragmentElement(fragment, this.#logsList);
this.#scrollToBottom();
};

/**
* @public
* @description Displays a summary of logs, grouping warnings and errors.
* Summaries include links to the corresponding log lines.
* Adds a click event listener to handle navigation within the summary.
* Prevents displaying a summary if no logs are present.
*/
public displaySummary(): void {
if (!this.#logsList.hasChildNodes()) {
console.warn('Cannot display summary because logs are empty');
return;
}

const fragment = document.createDocumentFragment();

if (this.#warnings.length > 0) {
const warningsSummary = this.#createSummary(SeverityClasses.WARNING, this.#warnings);
fragment.appendChild(warningsSummary);
}

if (this.#errors.length > 0) {
const errorsSummary = this.#createSummary(SeverityClasses.ERROR, this.#errors);
fragment.appendChild(errorsSummary);
}

if (fragment.hasChildNodes()) {
this.#logsSummary.addEventListener('click', this.#handleLinkEvent);
}

this.#appendFragmentElement(fragment, this.#logsSummary);
this.#isSummaryDisplayed = true;
}

/**
* @private
* @param {LogEntry} logEntry - Parsed log entry containing message and severity information.
* @returns {HTMLDivElement} - The created log line element.
* @description Creates an HTML log line element based on the log entry's severity and message.
* Applies appropriate CSS classes and data attributes to the log line.
*/
#createLogLine = (logEntry: LogEntry): HTMLDivElement => {
const logLineFragment = this.#templateLogLine.content.cloneNode(true) as DocumentFragment;
const logLine = logLineFragment.querySelector('.logs__line') as HTMLDivElement;

logLine.classList.add(`logs__line--${logEntry.className}`);
logLine.setAttribute('data-status', logEntry.className);
logLine.textContent = logEntry.message;

return logLine;
};

/**
* @private
* @param {DocumentFragment} fragment - The fragment containing child elements to append.
* @param {HTMLElement} element - The target element to which the fragment will be appended.
* @description Appends a document fragment to a specified HTML element in the DOM.
*/
#appendFragmentElement = (fragment: DocumentFragment, element: HTMLElement) => {
element.appendChild(fragment);
};

/**
* @private
* @description Automatically scrolls the logs container to the bottom of the list.
*/
#scrollToBottom = () => {
this.#logsScroll.scrollTop = this.#logsScroll.scrollHeight;
};

/**
* @private
* @param {SeverityClasses} severity - The severity type (e.g., WARNING, ERROR).
* @param {string[]} logs - Array of log IDs to include in the summary.
* @returns {HTMLDivElement} - The created summary element.
* @description Creates a summary element grouping logs by severity.
* Each log in the summary includes a link to its corresponding log line.
*/
#createSummary(severity: SeverityClasses, logs: string[]): HTMLDivElement {
const summaryFragment = this.#templateSummary.content.cloneNode(true) as DocumentFragment;
const summary = summaryFragment.querySelector('.logs__summary') as HTMLDivElement;

const title = this.#getSummaryTitle(severity);
const titleContainer = summary.querySelector('[data-slot-template="title"]') as HTMLDivElement;
titleContainer.textContent = title;

const linkElement = this.#createSummaryLinkElement(severity);

logs.forEach((logId) => {
const logElement = document.getElementById(logId)!;
const cloneLogElement = logElement.cloneNode(true) as HTMLDivElement;
cloneLogElement.id = '';

const linkClone = linkElement.cloneNode(true) as HTMLAnchorElement;
linkClone.href = `#${logId}`;

cloneLogElement.appendChild(linkClone);

summary.appendChild(cloneLogElement);
});

return summary;
}

/**
* @private
* @param {SeverityClasses} severity - The severity type (e.g., WARNING, ERROR).
* @returns {string} - The content of the title template.
* @description Retrieves the title template for the given severity type and extracts its content.
*/
#getSummaryTitle(severity: SeverityClasses): string {
const titleTemplate = this.queryElement<HTMLTemplateElement>(
`#summary-${severity}-title`,
`Summary ${severity} title not found`
);

const title = titleTemplate.content.cloneNode(true) as HTMLElement;

return title.textContent!;
}

/**
* @private
* @param {SeverityClasses} severity - The severity type (e.g., WARNING, ERROR).
* @returns {HTMLAnchorElement} - The created link element.
* @description Creates a link element from the template corresponding to the given severity type.
*/
#createSummaryLinkElement(severity: SeverityClasses): HTMLAnchorElement {
const linkTemplate = this.queryElement<HTMLTemplateElement>(
`#summary-${severity}-link`,
`Summary ${severity} link not found`
);

const linkFragment = linkTemplate.content.cloneNode(true) as DocumentFragment;
return linkFragment.querySelector('.link') as HTMLAnchorElement;
}

/**
* @private
* @param {MouseEvent} event - The click event object.
* @description Handles click events on summary links to scroll to the corresponding log line.
* Highlights the target log line briefly for visual focus.
*/
#handleLinkEvent = (event: MouseEvent): void => {
const target = event.target as HTMLAnchorElement;

// Checks if the clicked element is an <a> tag
if (!target || target.tagName !== 'A' || !target.hash) {
return;
}

event.preventDefault();

const logId = target.hash.substring(1);
const targetElement = document.getElementById(logId);

if (targetElement) {
const scrollTop = targetElement.offsetTop - this.#logsScroll.offsetTop;
this.#logsScroll.scrollTop = scrollTop;
targetElement.classList.add('logs__line--pointed');
window.setTimeout(() => {
targetElement.classList.remove('logs__line--pointed');
}, 2000);
}
};
}
Loading

0 comments on commit a1ee13e

Please sign in to comment.