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

fix: sidebar problems #885

Merged
merged 6 commits into from
Jan 17, 2024
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
85 changes: 29 additions & 56 deletions library/src/containers/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PUBLISH_LABEL_DEFAULT_TEXT,
SUBSCRIBE_LABEL_DEFAULT_TEXT,
} from '../../constants';
import { TagObject, filterObjectsByTags } from '../../helpers/sidebar';

const SidebarContext = React.createContext<{
setShowSidebar: React.Dispatch<React.SetStateAction<boolean>>;
Expand Down Expand Up @@ -168,52 +169,11 @@ export const Sidebar: React.FunctionComponent = () => {
);
};

interface TagObject<T = any> {
name: string;
object: { tags?: () => Array<{ name: () => string }> };
data: T;
}

function filterObjectsByTags<T = any>(
tags: string[],
objects: Array<TagObject<T>>,
): { tagged: Map<string, TagObject[]>; untagged: TagObject[] } {
const taggedObjects = new Set<TagObject>();
const tagged = new Map<string, TagObject[]>();

tags.forEach(tag => {
const taggedForTag: TagObject[] = [];
objects.forEach(obj => {
const object = obj.object;
if (typeof object.tags !== 'function') {
return;
}

const objectTags = (object.tags() || []).map(t => t.name());
const hasTag = objectTags.includes(tag);
if (hasTag) {
taggedForTag.push(obj);
taggedObjects.add(obj);
}
});
tagged.set(tag, taggedForTag);
});

const untagged: TagObject[] = [];
objects.forEach(obj => {
if (!taggedObjects.has(obj)) {
untagged.push(obj);
}
});

return { tagged, untagged };
}

const ServersList: React.FunctionComponent = () => {
const sidebarConfig = useConfig().sidebar;
const asyncapi = useSpec();
const servers = asyncapi.servers().all();
const showServers = sidebarConfig?.showServers || 'byDefault';
const showServers = sidebarConfig?.showServers ?? 'byDefault';

if (showServers === 'byDefault') {
return (
Expand All @@ -227,7 +187,12 @@ const ServersList: React.FunctionComponent = () => {

let specTagNames: string[];
if (showServers === 'bySpecTags') {
specTagNames = (asyncapi.info().tags() || []).map(tag => tag.name());
specTagNames = (
asyncapi
.info()
.tags()
.all() ?? []
).map(tag => tag.name());
} else {
const serverTagNamesSet = new Set<string>();
servers.forEach(server => {
Expand All @@ -238,7 +203,7 @@ const ServersList: React.FunctionComponent = () => {

const serializedServers: TagObject[] = servers.map(server => ({
name: server.id(),
object: server,
tags: server.tags(),
data: {},
}));
const { tagged, untagged } = filterObjectsByTags(
Expand Down Expand Up @@ -273,7 +238,7 @@ const OperationsList: React.FunctionComponent = () => {
const sidebarConfig = useConfig().sidebar;
const asyncapi = useSpec();
const operations = asyncapi.operations().all();
const showOperations = sidebarConfig?.showOperations || 'byDefault';
const showOperations = sidebarConfig?.showOperations ?? 'byDefault';

const processedOperations: Array<TagObject<{
channelName: string;
Expand All @@ -287,22 +252,22 @@ const OperationsList: React.FunctionComponent = () => {
if (operation.isSend()) {
processedOperations.push({
name: `publish-${operation.id()}`,
object: operation,
tags: operation.tags(),
data: {
channelName: channelAddress || '',
channelName: channelAddress ?? '',
kind: 'publish',
summary: operation.summary() || '',
summary: operation.summary() ?? '',
},
});
}
if (operation.isReceive()) {
processedOperations.push({
name: `subscribe-${operation.id()}`,
object: operation,
tags: operation.tags(),
data: {
channelName: channelAddress || '',
channelName: channelAddress ?? '',
kind: 'subscribe',
summary: operation.summary() || '',
summary: operation.summary() ?? '',
},
});
}
Expand All @@ -320,11 +285,19 @@ const OperationsList: React.FunctionComponent = () => {

let operationTagNames: string[];
if (showOperations === 'bySpecTags') {
operationTagNames = (asyncapi.info().tags() || []).map(tag => tag.name());
operationTagNames = (
asyncapi
.info()
.tags()
.all() ?? []
).map(tag => tag.name());
} else {
const operationTagNamesSet = new Set<string>();
operations.forEach(operation => {
operation.tags().forEach(t => operationTagNamesSet.add(t.name()));
operation
.tags()
.all()
.forEach(t => operationTagNamesSet.add(t.name()));
});
operationTagNames = Array.from(operationTagNamesSet);
}
Expand Down Expand Up @@ -374,9 +347,9 @@ const OperationItem: React.FunctionComponent<OperationItemProps> = ({
const isPublish = kind === 'publish';
let label: string = '';
if (isPublish) {
label = config.publishLabel || PUBLISH_LABEL_DEFAULT_TEXT;
label = config.publishLabel ?? PUBLISH_LABEL_DEFAULT_TEXT;
} else {
label = config.subscribeLabel || SUBSCRIBE_LABEL_DEFAULT_TEXT;
label = config.subscribeLabel ?? SUBSCRIBE_LABEL_DEFAULT_TEXT;
}

return (
Expand All @@ -394,7 +367,7 @@ const OperationItem: React.FunctionComponent<OperationItemProps> = ({
>
{label}
</span>
<span className="break-all inline-block">{summary || channelName}</span>
<span className="break-all inline-block">{summary ?? channelName}</span>
</a>
</li>
);
Expand Down
97 changes: 97 additions & 0 deletions library/src/containers/Sidebar/__tests__/SideBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @jest-environment jsdom
*/

import React from 'react';
import { render } from '@testing-library/react';
import { Sidebar } from '../Sidebar';
import { ConfigContext, SpecificationContext } from '../../../contexts';
import asyncapi from '../../../__tests__/docs/v3/streetlights-kafka.json';
import { Parser } from '../../../helpers';
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';
describe('Sidebar component', () => {
let parsed: AsyncAPIDocumentInterface;
beforeAll(async () => {
const parsedDoc = await Parser.parse(asyncapi, {});
expect(parsedDoc.error).toBeUndefined();
expect(parsedDoc.asyncapi).toBeDefined();
parsed = parsedDoc.asyncapi!;
});
test('should render sidebar with showOperations: byDefault', async () => {
render(
<ConfigContext.Provider
value={{ sidebar: { showOperations: 'byDefault' } }}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render sidebar with showOperations: byOperationsTags', async () => {
render(
<ConfigContext.Provider
value={{ sidebar: { showOperations: 'byOperationsTags' } }}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render sidebar with showOperations: bySpecTags', async () => {
render(
<ConfigContext.Provider
value={{ sidebar: { showOperations: 'bySpecTags' } }}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render sidebar with showServers: byDefault', async () => {
render(
<ConfigContext.Provider value={{ sidebar: { showServers: 'byDefault' } }}>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render sidebar with showServers: byServersTags', async () => {
render(
<ConfigContext.Provider
value={{ sidebar: { showServers: 'byServersTags' } }}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render sidebar with showServers: bySpecTags', async () => {
render(
<ConfigContext.Provider
value={{ sidebar: { showServers: 'bySpecTags' } }}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
test('should render with showOperations: byDefault, showServers: byDefault', async () => {
render(
<ConfigContext.Provider
value={{
sidebar: { showOperations: 'byDefault', showServers: 'byDefault' },
}}
>
<SpecificationContext.Provider value={parsed}>
<Sidebar />
</SpecificationContext.Provider>
</ConfigContext.Provider>,
);
});
});
47 changes: 47 additions & 0 deletions library/src/helpers/__tests__/sidebar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { OperationInterface, TagV2, TagsV2 } from '@asyncapi/parser';
import { TagObject, filterObjectsByTags } from '../sidebar';

describe('sidebar', () => {
describe('.filterObjectsByTags', () => {
test('should handle empty objects and find nothing', () => {
const tagsToFind = ['test'];
const objects: Array<TagObject<OperationInterface>> = [];
const filteredTags = filterObjectsByTags(tagsToFind, objects);
expect(filteredTags.tagged.size).toEqual(0);
expect(filteredTags.untagged.length).toEqual(0);
});
test('should handle find one instance', () => {
const tagsToFind = ['test'];
const tagsToSearch = new TagsV2([new TagV2({ name: 'test' })]);
const objects: Array<TagObject<any>> = [
{ data: {}, name: '', tags: tagsToSearch },
];
const filteredTags = filterObjectsByTags(tagsToFind, objects);
expect(filteredTags.tagged.size).toEqual(1);
expect(filteredTags.untagged.length).toEqual(0);
});
test('should handle find multiple instances', () => {
const tagsToFind = ['test'];
const obj1 = {
data: {},
name: '',
tags: new TagsV2([new TagV2({ name: 'test' })]),
};
const obj2 = {
data: {},
name: '',
tags: new TagsV2([new TagV2({ name: 'none' })]),
};
const obj3 = {
data: {},
name: '',
tags: new TagsV2([new TagV2({ name: 'test' })]),
};
const objects: Array<TagObject<any>> = [obj1, obj2, obj3];
const filteredTags = filterObjectsByTags(tagsToFind, objects);
expect(filteredTags.tagged.size).toEqual(1);
expect(filteredTags.tagged.get('test')!.length).toEqual(2);
expect(filteredTags.untagged.length).toEqual(1);
});
});
});
46 changes: 46 additions & 0 deletions library/src/helpers/sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TagsInterface } from '@asyncapi/parser';

export interface TagObject<T = any> {
name: string;
tags: TagsInterface;
data: T;
}
export interface SortedReturnType {
tagged: Map<string, TagObject[]>;
untagged: TagObject[];
}

/**
* Filter an array of objects by certain tags
*/
export function filterObjectsByTags<T>(
tags: string[],
objects: Array<TagObject<T>>,
): SortedReturnType {
const taggedObjects = new Set<TagObject>();
const tagged = new Map<string, TagObject[]>();
tags.forEach(tag => {
const taggedForTag: TagObject[] = [];
objects.forEach(obj => {
const objTags = obj.tags;
const nameTags = (objTags.all() ?? []).map(t => t.name());
const hasTag = nameTags.includes(tag);
if (hasTag) {
taggedForTag.push(obj);
taggedObjects.add(obj);
}
});
if (taggedForTag.length > 0) {
tagged.set(tag, taggedForTag);
}
});

const untagged: TagObject[] = [];
objects.forEach(obj => {
if (!taggedObjects.has(obj)) {
untagged.push(obj);
}
});

return { tagged, untagged };
}