Skip to content

FUI - Product Subscriptions list #2510

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 3 commits into from
Jun 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
2 changes: 2 additions & 0 deletions src/apim.runtime.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { ProductListRuntimeModule } from "./components/products/product-list/pro
import { ProductApisRuntimeModule } from "./components/products/product-apis/productApis.runtime.module";
import { OperationListRuntimeModule } from "./components/operations/operation-list/operationList.runtime.module";
import { DetailsOfApiRuntimeModule } from "./components/apis/details-of-api/detailsOfApi.runtime.module";
import { ProductSubscriptionsRuntimeModule } from "./components/products/product-subscriptions/productSubscriptions.runtime.module";

export class ApimRuntimeModule implements IInjectorModule {
public register(injector: IInjector): void {
Expand Down Expand Up @@ -178,6 +179,7 @@ export class ApimRuntimeModule implements IInjectorModule {
injector.bindModule(new ProductApisRuntimeModule());
injector.bindModule(new OperationListRuntimeModule());
injector.bindModule(new DetailsOfApiRuntimeModule());
injector.bindModule(new ProductSubscriptionsRuntimeModule());

if (process.env.NODE_ENV === staticDataEnvironment) {
injector.bind("httpClient", StaticDataHttpClient);
Expand Down
60 changes: 29 additions & 31 deletions src/components/apis/list-of-apis/react/runtime/ApisTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,37 +85,35 @@ const TableBodyTags = ({ tags, ...props }: Props & { tags: Page<TagGroup<Api>> }
};

export const ApisTable = ({ apis, ...props }: Props & { apis: TApisData }) => (
<div className={"fui-table"}>
<Table size={"small"} aria-label={"APIs List table"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell>
<Body1Strong>Name</Body1Strong>
<Table className={"fui-table"} size={"small"} aria-label={"APIs List table"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell>
<Body1Strong>Name</Body1Strong>
</TableHeaderCell>
<TableHeaderCell>
<Body1Strong>Description</Body1Strong>
</TableHeaderCell>
{props.showApiType && (
<TableHeaderCell style={{ width: "8em" }}>
<Body1Strong>Type</Body1Strong>
</TableHeaderCell>
<TableHeaderCell>
<Body1Strong>Description</Body1Strong>
</TableHeaderCell>
{props.showApiType && (
<TableHeaderCell style={{ width: "8em" }}>
<Body1Strong>Type</Body1Strong>
</TableHeaderCell>
)}
</TableRow>
</TableHeader>

<TableBody>
{isApisGrouped(apis) ? (
<TableBodyTags
{...props}
tags={apis}
/>
) : (
<TableBodyApis
{...props}
apis={apis.value}
/>
)}
</TableBody>
</Table>
</div>
</TableRow>
</TableHeader>

<TableBody>
{isApisGrouped(apis) ? (
<TableBodyTags
{...props}
tags={apis}
/>
) : (
<TableBodyApis
{...props}
apis={apis.value}
/>
)}
</TableBody>
</Table>
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Combobox,
Link,
Option,
OptionGroup,
Spinner,
} from "@fluentui/react-components";
import { Resolve } from "@paperbits/react/decorators";
Expand Down Expand Up @@ -50,10 +49,10 @@ const ProductsDropdownFC = ({
selectedProduct,
statePageNumber: [pageNumber, setPageNumber],
statePattern: [_, setPattern],
}: TProductListDropdown & { selectedProduct?: Product }) => {
}: TProductListDropdown & { selectedProduct?: Product | null }) => {
const pageMax = Math.ceil(products?.count / Constants.defaultPageSize);

const content = !products || !selectedProduct ? (
const content = !products || selectedProduct === undefined ? (
<>Loading Products</> // if data are not loaded yet ComboBox sometimes fails to initialize properly - edge case, in most cases almost instant from the cache
) : (
<Combobox
Expand Down Expand Up @@ -109,7 +108,7 @@ const ProductsDropdownFC = ({

export class ProductsDropdown extends React.Component<
TProductListDropdown,
{ working: boolean; selectedProduct?: Product }
{ working: boolean; selectedProduct?: Product | null }
> {
@Resolve("productService")
public productService: ProductService;
Expand All @@ -132,7 +131,10 @@ export class ProductsDropdown extends React.Component<

async loadSelectedProduct() {
const productName = this.routeHelper.getProductName();
if (!productName) return;
if (!productName) {
this.setState({ selectedProduct: null });
return;
}

this.setState({ working: true, selectedProduct: undefined });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,23 @@ const TableBodyProducts = ({ products, getReferenceUrl }: Props & { products: Pr
);

export const ProductsTable = ({ products, getReferenceUrl }: Props & { products: TProductsData }) => (
<div className={"fui-table"}>
<Table size={"small"} aria-label={"Products List table"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell>
<Body1Strong>Name</Body1Strong>
</TableHeaderCell>
<TableHeaderCell>
<Body1Strong>Description</Body1Strong>
</TableHeaderCell>
</TableRow>
</TableHeader>
<Table className={"fui-table"} size={"small"} aria-label={"Products List table"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell>
<Body1Strong>Name</Body1Strong>
</TableHeaderCell>
<TableHeaderCell>
<Body1Strong>Description</Body1Strong>
</TableHeaderCell>
</TableRow>
</TableHeader>

<TableBody>
<TableBodyProducts
products={products.value}
getReferenceUrl={getReferenceUrl}
/>
</TableBody>
</Table>
</div>
<TableBody>
<TableBodyProducts
products={products.value}
getReferenceUrl={getReferenceUrl}
/>
</TableBody>
</Table>
);
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<!-- ko if: isRedesignEnabled -->
<fui-product-subscriptions-runtime></fui-product-subscriptions-runtime>
<!-- /ko -->

<!-- ko ifnot: isRedesignEnabled -->
<product-subscriptions-runtime></product-subscriptions-runtime>
<!-- /ko -->
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { StyleModel } from "@paperbits/common/styles";
})
export class ProductSubscriptionsViewModel {
public readonly styles: ko.Observable<StyleModel>;

public readonly isRedesignEnabled: ko.Observable<boolean>;

constructor() {
this.styles = ko.observable<StyleModel>();
this.isRedesignEnabled = ko.observable<boolean>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { StyleCompiler } from "@paperbits/common/styles";
import { ViewModelBinder, WidgetState } from "@paperbits/common/widgets";
import { ProductSubscriptionsModel } from "../productSubscriptionsModel";
import { ProductSubscriptionsViewModel } from "./productSubscriptionsViewModel";
import { isRedesignEnabledSetting } from "../../../../constants";
import { ISiteService } from "@paperbits/common/sites";


export class ProductSubscriptionsViewModelBinder implements ViewModelBinder<ProductSubscriptionsModel, ProductSubscriptionsViewModel> {
constructor( private readonly styleCompiler: StyleCompiler) { }
constructor(
private readonly styleCompiler: StyleCompiler,
private readonly siteService: ISiteService,
) { }

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

public async modelToState(model: ProductSubscriptionsModel, state: WidgetState): Promise<void> {
if (model.styles) {
state.styles = await this.styleCompiler.getStyleModelAsync(model.styles);
}
state.isRedesignEnabled = !!(await this.siteService.getSetting(isRedesignEnabledSetting));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IInjector, IInjectorModule } from "@paperbits/common/injection";
import { ProductSubscriptionsRuntime } from "./react/ProductSubscriptionsRuntime";
import { registerCustomElement } from "@paperbits/react/customElements";

export class ProductSubscriptionsRuntimeModule implements IInjectorModule {
public register(injector: IInjector): void {
injector.bind("productSubscriptionsRuntime", ProductSubscriptionsRuntime);
registerCustomElement(ProductSubscriptionsRuntime, "fui-product-subscriptions-runtime", injector);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from "react";
import { useEffect, useState } from "react";
import { FluentProvider } from "@fluentui/react-components";
import { Resolve } from "@paperbits/react/decorators";
import { Router } from "@paperbits/common/routing";
import * as Constants from "../../../../constants";
import { RouteHelper } from "../../../../routing/routeHelper";
import { UsersService } from "../../../../services";
import { ProductService } from "../../../../services/productService";
import { Subscription } from "../../../../models/subscription";
import { ProductSubscriptionsTable } from "./ProductSubscriptionsTable";

type ProductSubscriptionsProps = {}
type ProductSubscriptionsFCProps = ProductSubscriptionsProps & {
usersService: UsersService;
productService: ProductService;
productName: string;
};

const loadSubscriptions = async (usersService: UsersService, productService: ProductService, productName: string) => {
const userId = await usersService.getCurrentUserId();
const { value: subscriptions } =
await productService.getSubscriptionsForProduct(userId, `/products/${productName}`);

return { userId, subscriptions };
};

const ProductSubscriptionsRuntimeFC = ({ usersService, productService, productName }: ProductSubscriptionsFCProps) => {
const [working, setWorking] = useState(false);
const [subscriptions, setSubscriptions] = useState<Subscription[]>();

useEffect(() => {
setWorking(true);
loadSubscriptions(usersService, productService, productName)
.then(({ subscriptions }) => setSubscriptions(subscriptions))
.catch((error) => {
if (error.code === "Unauthorized") {
usersService.navigateToSignin();
return;
}
// TODO better error handling & logging
if (error.code === "ResourceNotFound") return;

throw new Error(`Could not load product subscriptions. Error: ${error.message}`);
})
.finally(() => setWorking(false));
}, [usersService, productService, productName]);

return <ProductSubscriptionsTable subscriptions={subscriptions} working={working} />;
};

export class ProductSubscriptionsRuntime extends React.Component<
ProductSubscriptionsProps,
{ productName?: string | null }
> {
@Resolve("usersService")
public usersService: UsersService;

@Resolve("productService")
public productService: ProductService;

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

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

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

this.state = { productName: undefined };
}

componentDidMount() {
this.setProductName();
this.router.addRouteChangeListener(this.setProductName.bind(this));
}

componentWillUnmount() {
this.router.removeRouteChangeListener(this.setProductName.bind(this));
}

setProductName() {
this.setState({ productName: this.routeHelper.getProductName() });
}

render() {
if (!this.state.productName) return <>Please select a product</>;

return (
<FluentProvider theme={Constants.fuiTheme}>
<ProductSubscriptionsRuntimeFC
{...this.props}
usersService={this.usersService}
productService={this.productService}
productName={this.state.productName}
/>
</FluentProvider>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react";
import {
Body1Strong,
Spinner,
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "@fluentui/react-components";
import { Subscription } from "../../../../models/subscription";

export const ProductSubscriptionsTable = ({
subscriptions,
working,
}: {
subscriptions: Subscription[];
working: boolean;
}) =>
working ? (
<Spinner
label="Loading Subsriptions"
labelPosition="below"
size="small"
/>
) : !subscriptions || subscriptions.length === 0 ? (
<Body1Strong>You don't have subscriptions yet.</Body1Strong>
) : (
<Table className={"fui-table"} size={"small"} aria-label={"Your Subscriptions list"}>
<TableHeader>
<TableRow className={"fui-table-headerRow"}>
<TableHeaderCell>
<Body1Strong>Name</Body1Strong>
</TableHeaderCell>
<TableHeaderCell>
<Body1Strong>Status</Body1Strong>
</TableHeaderCell>
</TableRow>
</TableHeader>

<TableBody>
{subscriptions?.map((sub) => (
<TableRow key={sub.id}>
<TableCell>{sub.name}</TableCell>
<TableCell>{sub.state}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);