Skip to content

FUI - API changelog page #2675

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

Merged
merged 1 commit into from
Sep 18, 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
2 changes: 2 additions & 0 deletions src/apim.runtime.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import { ListOfApisRuntimeModule } from "./components/apis/list-of-apis/listOfAp
import { ProductListRuntimeModule } from "./components/products/product-list/productList.runtime.module";
import { OperationListRuntimeModule } from "./components/operations/operation-list/operationList.runtime.module";
import { DetailsOfApiRuntimeModule } from "./components/apis/details-of-api/detailsOfApi.runtime.module";
import { HistoryOfApiRuntimeModule } from "./components/apis/history-of-api/historyOfApi.runtime.module";
import { OperationDetailsRuntimeModule } from "./components/operations/operation-details/operationDetails.runtime.module";
import { ProductApisRuntimeModule } from "./components/products/product-apis/productApis.runtime.module";
import { ProductDetailsRuntimeModule } from "./components/products/product-details/productDetails.runtime.module";
Expand Down Expand Up @@ -191,6 +192,7 @@ export class ApimRuntimeModule implements IInjectorModule {
injector.bindModule(new ProductApisRuntimeModule());
injector.bindModule(new OperationListRuntimeModule());
injector.bindModule(new DetailsOfApiRuntimeModule());
injector.bindModule(new HistoryOfApiRuntimeModule());
injector.bindModule(new OperationDetailsRuntimeModule());
injector.bindModule(new ProductDetailsRuntimeModule());
injector.bindModule(new ProductSubscribeRuntimeModule());
Expand Down
10 changes: 10 additions & 0 deletions src/components/apis/history-of-api/historyOfApi.runtime.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IInjector, IInjectorModule } from "@paperbits/common/injection";
import { registerCustomElement } from "@paperbits/react/customElements";
import { ApiHistory } from "./react/runtime/ApiHistory";

export class HistoryOfApiRuntimeModule implements IInjectorModule {
public register(injector: IInjector): void {
injector.bind("historyOfApiRuntime", ApiHistory);
registerCustomElement(ApiHistory, "fui-api-history", injector);
}
}
7 changes: 6 additions & 1 deletion src/components/apis/history-of-api/ko/historyOfApi.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<api-history data-bind="attr: { params: runtimeConfig }"></api-history>
<!-- ko if: isRedesignEnabled -->
<fui-api-history data-bind="attr: { props: runtimeConfig }"></fui-api-history>
<!-- /ko -->
<!-- ko ifnot: isRedesignEnabled -->
<api-history data-bind="attr: { params: runtimeConfig }"></api-history>
<!-- /ko -->
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { StyleModel } from "@paperbits/common/styles";
export class HistoryOfApiViewModel {
public readonly runtimeConfig: ko.Observable<string>;
public readonly styles: ko.Observable<StyleModel>;
public readonly isRedesignEnabled: ko.Observable<boolean>;

constructor() {
this.runtimeConfig = ko.observable();
this.styles = ko.observable<StyleModel>();
this.isRedesignEnabled = ko.observable<boolean>();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { ViewModelBinder, WidgetState } from "@paperbits/common/widgets";
import { StyleCompiler } from "@paperbits/common/styles";
import { ISiteService } from "@paperbits/common/sites";
import { HistoryOfApiViewModel } from "./historyOfApiViewModel";
import { HistoryOfApiModel } from "../historyOfApiModel";
import { StyleCompiler } from "@paperbits/common/styles";
import { isRedesignEnabledSetting } from "../../../../constants";


export class HistoryOfApiViewModelBinder implements ViewModelBinder<HistoryOfApiModel, HistoryOfApiViewModel> {
constructor(private readonly styleCompiler: StyleCompiler) { }
constructor(
private readonly styleCompiler: StyleCompiler,
private readonly siteService: ISiteService,
) { }

public stateToInstance(state: WidgetState, componentInstance: HistoryOfApiViewModel): void {
componentInstance.styles(state.styles);
componentInstance.isRedesignEnabled(state.isRedesignEnabled);

componentInstance.runtimeConfig(JSON.stringify({
detailsPageUrl: state.detailsPageUrl
Expand All @@ -23,5 +29,7 @@ export class HistoryOfApiViewModelBinder implements ViewModelBinder<HistoryOfApi
if (model.styles) {
state.styles = await this.styleCompiler.getStyleModelAsync(model.styles);
}

state.isRedesignEnabled = !!(await this.siteService.getSetting(isRedesignEnabledSetting));
}
}
174 changes: 174 additions & 0 deletions src/components/apis/history-of-api/react/runtime/ApiHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as React from "react";
import { useEffect, useState } from "react";
import { Resolve } from "@paperbits/react/decorators";
import { Router } from "@paperbits/common/routing";
import { Stack } from "@fluentui/react";
import { Body1, Body1Strong, FluentProvider, Link, Spinner, Subtitle1, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow } from "@fluentui/react-components";
import { Api } from "../../../../../models/api";
import { Page } from "../../../../../models/page";
import { ChangeLogContract } from "../../../../../contracts/apiChangeLog";
import { ApiService } from "../../../../../services/apiService";
import { RouteHelper } from "../../../../../routing/routeHelper";
import { Pagination } from "../../../../utils/react/Pagination";
import { defaultPageSize, fuiTheme } from "../../../../../constants";
import { Utils } from "../../../../../utils";

interface ApiHistoryProps {
detailsPageUrl?: string
}

interface ApiHistoryState {
apiName: string
}

const ApiHistoryFC = ({
apiName,
apiService,
routeHelper,
detailsPageUrl
}: ApiHistoryProps & { apiName: string, apiService: ApiService, routeHelper: RouteHelper }) => {
const [working, setWorking] = useState<boolean>(false);
const [api, setApi] = useState<Api>();
const [currentChangelogPage, setCurrentChangelogPage] = useState<Page<ChangeLogContract>>();
const [pageNumber, setPageNumber] = useState<number>(1);

useEffect(() => {
if (apiName) {
setWorking(true);
loadApi().then(api => {
setApi(api);
loadChangelogPage(api.id).then(changelogPage => setCurrentChangelogPage(changelogPage));
}).finally(() => setWorking(false));
}
}, [apiName, apiService]);

useEffect(() => {
if (api) {
setWorking(true);
loadChangelogPage(api.id)
.then(changelogPage => setCurrentChangelogPage(changelogPage))
.finally(() => setWorking(false));
}
}, [pageNumber]);

const loadApi = async (): Promise<Api> => {
let api: Api;

try {
api = await apiService.getApi(`apis/${apiName}`);
} catch (error) {
throw new Error(`Unable to load the API. Error: ${error.message}`);
}

return api;
}

const loadChangelogPage = async (apiId?: string): Promise<Page<ChangeLogContract>> => {
let changelogPage: Page<ChangeLogContract>;

try {
changelogPage = await apiService.getApiChangeLog(apiId ?? api.id, (pageNumber - 1) * defaultPageSize);
} catch (error) {
throw new Error(`Unable to load the API history. Error: ${error.message}`);
}

return changelogPage;
}

return (
<>
{working
? <Spinner />
: api
? <>
<Stack horizontal horizontalAlign="space-between" verticalAlign="center" style={{ marginBottom: "2rem" }}>
<Stack horizontal verticalAlign="center">
<Subtitle1 className={"api-title"}>{api.displayName}</Subtitle1>
</Stack>
{detailsPageUrl &&
<Link href={routeHelper.getApiReferenceUrl(apiName, detailsPageUrl)}>Go back to the API reference page</Link>
}
</Stack>
<Table className={"fui-table"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell><Body1Strong>Release date</Body1Strong></TableHeaderCell>
<TableHeaderCell><Body1Strong>Notes</Body1Strong></TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{currentChangelogPage?.value?.length > 0
? currentChangelogPage.value.map((changelog, index) => (
<TableRow key={index}>
<TableCell>{Utils.formatDateTime(changelog.properties.createdDateTime)}</TableCell>
<TableCell>{changelog.properties.notes}</TableCell>
</TableRow>
))
: <TableRow>
<TableCell colSpan={2} style={{ textAlign: "center" }}>No records to show</TableCell>
</TableRow>
}
</TableBody>
</Table>
{currentChangelogPage?.count > 1 &&
<div className={"pagination-container"}>
<Pagination pageNumber={pageNumber} setPageNumber={setPageNumber} pageMax={Math.ceil(currentChangelogPage?.count / defaultPageSize)} />
</div>
}
</>
: <Body1>No API found.</Body1>
}

</>
);
}

export class ApiHistory extends React.Component<ApiHistoryProps, ApiHistoryState> {
@Resolve("apiService")
public apiService: ApiService;

@Resolve("routeHelper")
public routeHelper: RouteHelper;

@Resolve("router")
public router: Router;

constructor(props: ApiHistoryProps) {
super(props);

this.state = {
apiName: null
}
}

componentDidMount(): void {
this.getApi();
this.router.addRouteChangeListener(() => this.getApi());
}

componentWillUnmount(): void {
this.router.removeRouteChangeListener(() => this.getApi());
}

getApi = (): void => {
const apiName = this.routeHelper.getApiName();

if (apiName && apiName !== this.state.apiName) {
this.setState({ apiName });
}
}


render() {
return (
<FluentProvider theme={fuiTheme}>
<ApiHistoryFC
{...this.props}
apiName={this.state.apiName}
apiService={this.apiService}
routeHelper={this.routeHelper}
/>
</FluentProvider>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const ReportsTable = <T extends unknown>({ mainLabel, pageState: [pageNum
{working ? (
<Spinner label={"Loading products"} labelPosition="below" style={{ marginBottom: "1.5rem" }} size="small" />
) : data?.value.length > 0 && (
<div style={{ display: "flex", justifyContent: "center", marginBottom: "1.5rem" }}>
<div className={"pagination-container"}>
<Pagination pageNumber={pageNumber} setPageNumber={setPageNumber} pageMax={Math.ceil(data?.count / Constants.defaultPageSize)} />
</div>
)}
Expand Down
27 changes: 16 additions & 11 deletions src/components/utils/react/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { Dispatch, SetStateAction } from "react";
import { Stack } from "@fluentui/react";
import { Button } from "@fluentui/react-components";
import {
ChevronDoubleLeft20Regular,
ChevronDoubleRight20Regular,
ChevronLeft20Regular,
ChevronRight20Regular
ChevronDoubleLeft16Regular,
ChevronDoubleRight16Regular,
ChevronLeft16Regular,
ChevronRight16Regular
} from "@fluentui/react-icons";

export type PaginationProps = {
Expand All @@ -18,25 +18,30 @@ export const Pagination = ({ pageNumber, setPageNumber, ...props }: PaginationPr
("pageMax" in props ? props.pageMax < 2 : (!props.hasNextPage && pageNumber < 2)) ? <></> : (
<Stack horizontal tokens={{childrenGap: ".5rem"}}>
<Stack.Item>
<Button appearance="transparent" onClick={() => setPageNumber(1)} disabled={pageNumber === 1} icon={<ChevronDoubleLeft20Regular />}/>
<Button appearance="transparent" onClick={() => setPageNumber(prev => prev - 1)} disabled={pageNumber === 1} icon={<ChevronLeft20Regular />}/>
<Button appearance="transparent" onClick={() => setPageNumber(1)} disabled={pageNumber === 1} icon={<ChevronDoubleLeft16Regular />}/>
<Button appearance="transparent" onClick={() => setPageNumber(prev => prev - 1)} disabled={pageNumber === 1} icon={<ChevronLeft16Regular />}/>
</Stack.Item>

{"pageMax" in props && Array.from({length: props.pageMax}, (_, i) => i + 1).map(e => (
<Button key={e} appearance="transparent" onClick={() => setPageNumber(e)} size="small" icon={
pageNumber === e ? <b>{e}</b> : <>{e}</>
}/>
<Button
key={e}
appearance="transparent"
onClick={() => setPageNumber(e)}
size="small"
icon={pageNumber === e ? <b>{e}</b> : <>{e}</>}
className={"pagination-page-button"}
/>
))}

<Stack.Item>
<Button
appearance="transparent"
onClick={() => setPageNumber(prev => prev + 1)}
disabled={"pageMax" in props ? pageNumber === props.pageMax : !props.hasNextPage}
icon={<ChevronRight20Regular/>}
icon={<ChevronRight16Regular/>}
/>
{"pageMax" in props && (
<Button appearance="transparent" onClick={() => setPageNumber(props.pageMax)} disabled={pageNumber === props.pageMax} icon={<ChevronDoubleRight20Regular />}/>
<Button appearance="transparent" onClick={() => setPageNumber(props.pageMax)} disabled={pageNumber === props.pageMax} icon={<ChevronDoubleRight16Regular />}/>
)}
</Stack.Item>
</Stack>
Expand Down
10 changes: 10 additions & 0 deletions src/themes/website/styles/widgets/fui/fluentui.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
margin-top: 0;
}

.pagination-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
}

.pagination-page-button > span {
font-size: 1rem;
}

.operation-method {
color: #696969;

Expand Down