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

Initial commit ApiList widget for common lib #2733

Open
wants to merge 1 commit into
base: fui/master
Choose a base branch
from
Open
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
109 changes: 109 additions & 0 deletions src/common/api-list/ApiList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from "react";
import { useEffect, useState } from "react";
import { FluentProvider } from "@fluentui/react-components";
import { SearchQuery } from "../models/searchQuery";
import * as Constants from "../constants";
import { ApiService } from "../data/apiService";
import { TagService } from "../data/tagService";
import { ApiListTableCards } from "./ApiListTableCards";
import { FiltersPosition, TApisData, TLayout } from "../types";
import { Resolve } from "../decorators/resolve.decorator";

export interface ApiListProps {
allowSelection?: boolean;
allowViewSwitching?: boolean;
filtersPosition?: FiltersPosition;
showApiType?: boolean;
defaultGroupByTagToEnabled?: boolean;
detailsPageUrl: string;
detailsPageTarget: string;
layoutDefault: TLayout;
}

export type TApiListRuntimeFCProps = Omit<ApiListProps, "detailsPageUrl"> & {
apiService: ApiService;
tagService: TagService;
getReferenceUrl: (apiName: string) => string;
}


const loadApis = async (apiService: ApiService, query: SearchQuery, groupByTag?: boolean, productName?: string) => {
let apis: TApisData;

try {
// if (groupByTag) {
// apis = await apiService.getApisByTags(query);
// } else {
apis = await apiService.getApis(query);
// }
} catch (error) {
throw new Error(`Unable to load APIs. Error: ${error.message}`);
}

return apis;
}

const ApiListRuntimeFC = ({ apiService, defaultGroupByTagToEnabled, layoutDefault, ...props }: TApiListRuntimeFCProps) => {
const [working, setWorking] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [apis, setApis] = useState<TApisData>();
const [pattern, setPattern] = useState<string>();
const [groupByTag, setGroupByTag] = useState(!!defaultGroupByTagToEnabled);
const [filters, setFilters] = useState({tags: [] as string[]});

/**
* Loads page of APIs.
*/
useEffect(() => {
const query: SearchQuery = {
pattern,
tags: filters.tags.map(name => ({id: name, name})),
skip: (pageNumber - 1) * Constants.defaultPageSize,
take: Constants.defaultPageSize
};

setWorking(true);
loadApis(apiService, query, groupByTag)
.then(apis => setApis(apis))
.finally(() => setWorking(false));
}, [apiService, pageNumber, groupByTag, filters, pattern]);

return (
<ApiListTableCards
{...props}
layoutDefault={layoutDefault}
working={working}
apis={apis}
statePageNumber={[pageNumber, setPageNumber]}
statePattern={[pattern, setPattern]}
stateFilters={[filters, setFilters]}
stateGroupByTag={[groupByTag, setGroupByTag]}
/>
);
}

export class ApiList extends React.Component<ApiListProps> {
@Resolve("apiService")
public apiService: ApiService;

@Resolve("tagService")
public tagService: TagService;

//TODO: Implement getReferenceUrl
private getReferenceUrl(apiName: string): string {
return ""; //this.apiService.getApiReferenceUrl(apiName, this.props.detailsPageUrl);
}

render() {
return (
<FluentProvider theme={Constants.fuiTheme}>
<ApiListRuntimeFC
{...this.props}
apiService={this.apiService}
tagService={this.tagService}
getReferenceUrl={(apiName) => this.getReferenceUrl(apiName)}
/>
</FluentProvider>
);
}
}
116 changes: 116 additions & 0 deletions src/common/api-list/ApiListTableCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as React from "react";
import { useState } from "react";
import { Stack } from "@fluentui/react";
import { Spinner } from "@fluentui/react-components";
import * as Constants from "../constants";
import { ApisTable } from "./ApisTable";
import { ApisCards } from "./ApisCards";
import { TApiListRuntimeFCProps } from "./ApiList";
import { FiltersPosition, TApisData, TLayout } from "../types";
import { TableFiltersSidebar, TFilterActive } from "../utils/TableFilters";
import { useTableFiltersTags } from "../utils/useTableFiltersTags";
import { TableListInfo } from "../utils/TableListInfo";
import { Pagination } from "../utils/Pagination";

type TApiListTableCards = Omit<TApiListRuntimeFCProps, "apiService"> & {
working: boolean;
apis: TApisData;
statePageNumber: ReturnType<typeof useState<number>>;
statePattern: ReturnType<typeof useState<string>>;
stateFilters: ReturnType<typeof useState<TFilterActive>>;
stateGroupByTag: ReturnType<typeof useState<boolean>>;
};

export const ApiListTableCards = ({
tagService,
layoutDefault,
getReferenceUrl,
showApiType,
allowViewSwitching,
filtersPosition,
detailsPageTarget,
apis,
working,
statePageNumber: [pageNumber, setPageNumber],
statePattern: [pattern, setPattern],
stateFilters: [filters, setFilters],
stateGroupByTag: [groupByTag, setGroupByTag],
}: TApiListTableCards) => {
const [layout, setLayout] = useState<TLayout>(layoutDefault ?? TLayout.table);

const filterOptionTags = useTableFiltersTags(tagService);

const content = (
<Stack tokens={{ childrenGap: "1rem" }}>
<Stack.Item>
<TableListInfo
layout={layout}
setLayout={setLayout}
pattern={pattern}
setPattern={setPattern}
filters={filters}
setFilters={setFilters}
filtersOptions={filtersPosition === FiltersPosition.popup ? [filterOptionTags] : undefined}
setGroupByTag={setGroupByTag} // don't allow grouping by tags when filtering for product APIs due to missing BE support
allowViewSwitching={allowViewSwitching}
/>
</Stack.Item>

{working || !apis ? (
<Stack.Item>
<Spinner
label="Loading APIs"
labelPosition="below"
size="small"
/>
</Stack.Item>
) : (
<>
<Stack.Item style={{ marginTop: "2rem" }}>
{layout === TLayout.table ? (
<ApisTable
apis={apis}
showApiType={showApiType}
getReferenceUrl={getReferenceUrl}
detailsPageTarget={detailsPageTarget}
/>
) : (
<ApisCards
apis={apis}
showApiType={showApiType}
getReferenceUrl={getReferenceUrl}
detailsPageTarget={detailsPageTarget}
/>
)}
</Stack.Item>

<Stack.Item align={"center"}>
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pageMax={Math.ceil(apis?.count / Constants.defaultPageSize)}
/>
</Stack.Item>
</>
)}
</Stack>
);

return filtersPosition !== FiltersPosition.sidebar ? (
content
) : (
<Stack horizontal tokens={{ childrenGap: "2rem" }}>
<Stack.Item
shrink={0}
style={{ minWidth: "12rem", width: "15%", maxWidth: "20rem" }}
>
<TableFiltersSidebar
filtersActive={filters}
setFiltersActive={setFilters}
filtersOptions={groupByTag ? [] : [filterOptionTags]}
/>
</Stack.Item>
<Stack.Item style={{ width: "100%" }}>{content}</Stack.Item>
</Stack>
);
};
104 changes: 104 additions & 0 deletions src/common/api-list/ApisCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as React from "react";
import { useState } from "react";
import { Stack } from "@fluentui/react";
import { Api } from "../models/api";
import { TagGroup } from "../models/tagGroup";
import { MarkdownProcessor } from "../utils/MarkdownProcessor";
import { markdownMaxCharsMap } from "../constants";
import { isApisGrouped, toggleValueInSet, TagGroupToggleBtn, TApisData } from "./utils";

type Props = {
showApiType: boolean;
getReferenceUrl: (apiName: string) => string;
detailsPageTarget: string;
};

const ApiCard = ({ api, getReferenceUrl, showApiType, detailsPageTarget }: Props & { api: Api }) => {
return (
<div className={"fui-list-card"}>
<div style={{ height: "100%" }}>
{showApiType && (
<div className={"fui-list-card-tags"}>
<span className="caption1">API</span>
<span className="caption1">{api.typeName}</span>
</div>
)}
<h4>{api.displayName}</h4>

<MarkdownProcessor markdownToDisplay={api.description} maxChars={markdownMaxCharsMap.cards} />
</div>

<Stack horizontal>
<a
href={getReferenceUrl(api.name)}
target={detailsPageTarget}
title={api.displayName}
role="button"
className="button"
>
Go to API
</a>
</Stack>
</div>
);
};

const ApisCardsContainer = ({ apis, ...props }: Props & { apis: Api[] }) => (
<>
{apis?.length > 0
? <div className={"fui-list-cards-container"}>
{apis.map((api) => (
<ApiCard
{...props}
key={api.id}
api={api}
/>
))}
</div>
: <span style={{ textAlign: "center" }}>No APIs to display</span>
}
</>
);

const ApisGroupedCards = ({ tags, ...props }: Props & { tags: TagGroup<Api>[] }) => {
const [expanded, setExpanded] = useState(new Set());

return (
<div className={"fui-list-tag-cards-container"}>
{tags?.map(({ tag, items }) => (
<div key={tag}>
<button
className={"fui-list-tag-cards no-border"}
onClick={() => setExpanded(old => toggleValueInSet(old, tag))}
>
<TagGroupToggleBtn expanded={expanded.has(tag)}/>

<span className="strong" style={{marginLeft: ".5rem"}}>
{tag}
</span>
</button>

{expanded.has(tag) && (
<ApisCardsContainer
{...props}
apis={items}
/>
)}
</div>
))}
</div>
);
};

export const ApisCards = ({ apis, ...props }: Props & { apis: TApisData }) =>
isApisGrouped(apis) ? (
<ApisGroupedCards
{...props}
tags={apis.value}
/>
) : (
<ApisCardsContainer
{...props}
apis={apis.value}
/>
);
Loading