Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
51 changes: 51 additions & 0 deletions library/src/components/Highlight/Highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useEffect, useRef } from 'react';

interface HighlightProps {
elementId: string;
duration?: number;
}

export const Highlight: React.FC<HighlightProps> = ({ elementId, duration = 2000 }) => {
const highlightRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const element = document.getElementById(elementId);
if (!element || !highlightRef.current) return;

const rect = element.getBoundingClientRect();
const highlight = highlightRef.current;

// Position the highlight overlay
highlight.style.top = `${rect.top + window.scrollY}px`;
highlight.style.left = `${rect.left + window.scrollX}px`;
highlight.style.width = `${rect.width}px`;
highlight.style.height = `${rect.height}px`;
highlight.style.opacity = '1';

// Scroll the element into view
element.scrollIntoView({ behavior: 'smooth', block: 'center' });

// Fade out the highlight after the duration
const timeout = setTimeout(() => {
highlight.style.opacity = '0';
}, duration);

return () => clearTimeout(timeout);
}, [elementId, duration]);

return (
<div
ref={highlightRef}
style={{
position: 'absolute',
pointerEvents: 'none',
transition: 'opacity 0.5s ease-out',
opacity: '0',
backgroundColor: 'rgba(3, 169, 244, 0.1)',
border: '2px solid rgba(3, 169, 244, 0.4)',
borderRadius: '4px',
zIndex: 1000,
}}
/>
);
};
1 change: 1 addition & 0 deletions library/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './Markdown';
export * from './Schema';
export * from './Tag';
export * from './Tags';
export * from './Highlight/Highlight';
35 changes: 29 additions & 6 deletions library/src/containers/AsyncApi/AsyncApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';

import AsyncApiStandalone from './Standalone';
import { Highlight } from '../../components/Highlight/Highlight';

import {
isFetchingSchemaInterface,
Expand All @@ -10,6 +11,7 @@ import {
} from '../../types';
import { ConfigInterface } from '../../config';
import { SpecificationHelpers, Parser } from '../../helpers';
import { ChangeTracker } from '../../helpers/ChangeTracker';

export interface AsyncApiProps {
schema: PropsSchema;
Expand All @@ -19,13 +21,17 @@ export interface AsyncApiProps {
interface AsyncAPIState {
asyncapi?: AsyncAPIDocumentInterface;
error?: ErrorObject;
highlightedElement?: string;
}

class AsyncApiComponent extends Component<AsyncApiProps, AsyncAPIState> {
state: AsyncAPIState = {
asyncapi: undefined,
error: undefined,
highlightedElement: undefined,
};

private lastSchema: string | undefined;

async componentDidMount() {
if (this.props.schema) {
Expand All @@ -41,19 +47,36 @@ class AsyncApiComponent extends Component<AsyncApiProps, AsyncAPIState> {
if (oldSchema !== newSchema) {
const { config } = this.props;
await this.parseSchema(newSchema, config?.parserOptions);

// Detect changes and update highlight
if (typeof oldSchema === 'string' && typeof newSchema === 'string') {
const changes = await ChangeTracker.detectChanges(oldSchema, newSchema);
if (changes.length > 0) {
// Get the first changed element to highlight
this.setState({ highlightedElement: changes[0].elementId });
}
}
}
}

render() {
const { schema, config } = this.props;
const { asyncapi, error } = this.state;
const { asyncapi, error, highlightedElement } = this.state;

return (
<AsyncApiStandalone
schema={asyncapi ?? schema}
config={config}
error={error}
/>
<>
<AsyncApiStandalone
schema={asyncapi ?? schema}
config={config}
error={error}
/>
{highlightedElement && (
<Highlight
elementId={highlightedElement}
duration={2000}
/>
)}
</>
);
}

Expand Down
76 changes: 76 additions & 0 deletions library/src/helpers/ChangeTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';
import { Parser } from './Parser';

interface Change {
path: string[];
type: 'add' | 'remove' | 'modify';
elementId: string;
}

export class ChangeTracker {
private static getElementId(path: string[]): string {
return path.join('-');
}

private static async parseSchema(schema: string) {
try {
const parsed = await Parser.parse(schema);
return parsed;
} catch (e) {
return null;
}
}

static async detectChanges(oldSchema: string, newSchema: string): Promise<Change[]> {
const oldDoc = await this.parseSchema(oldSchema);
const newDoc = await this.parseSchema(newSchema);

if (!oldDoc || !newDoc) return [];

const changes: Change[] = [];

// Compare info section
if (JSON.stringify(oldDoc.info()) !== JSON.stringify(newDoc.info())) {
changes.push({
path: ['info'],
type: 'modify',
elementId: 'introduction'
});
}

// Compare servers
const oldServers = oldDoc.servers();
const newServers = newDoc.servers();
if (JSON.stringify(oldServers) !== JSON.stringify(newServers)) {
changes.push({
path: ['servers'],
type: 'modify',
elementId: 'servers'
});
}

// Compare channels
const oldChannels = oldDoc.channels();
const newChannels = newDoc.channels();
if (JSON.stringify(oldChannels) !== JSON.stringify(newChannels)) {
changes.push({
path: ['channels'],
type: 'modify',
elementId: 'operations'
});
}

// Compare schemas
const oldSchemas = oldDoc.components()?.schemas()?.all() || [];
const newSchemas = newDoc.components()?.schemas()?.all() || [];
if (JSON.stringify(oldSchemas) !== JSON.stringify(newSchemas)) {
changes.push({
path: ['components', 'schemas'],
type: 'modify',
elementId: 'schemas'
});
}

return changes;
}
}
Loading