193 lines
7.4 KiB
TypeScript
193 lines
7.4 KiB
TypeScript
import { Link, useParams, useSearchParams } from "react-router";
|
|
import { useEffect, useState } from "react";
|
|
import ApiKeyLogUtils from "../utils/ApiKeyLogUtils";
|
|
import { RowHeaderDisplayDto } from "~/application/dtos/data/RowHeaderDisplayDto";
|
|
import { ApiCallSummaryDto } from "../dtos/ApiCallSummaryDto";
|
|
import ApiKeyLogsConstants from "../utils/ApiKeyLogsConstants";
|
|
import { FilterablePropertyDto } from "~/application/dtos/data/FilterablePropertyDto";
|
|
import { FilterableValueLink } from "~/components/ui/links/FilterableValueLink";
|
|
import WarningBanner from "~/components/ui/banners/WarningBanner";
|
|
import InputCombobox from "~/components/ui/input/InputCombobox";
|
|
import InputFilters from "~/components/ui/input/InputFilters";
|
|
import TableSimple from "~/components/ui/tables/TableSimple";
|
|
import { useLocaleFormatters } from "~/locale/format";
|
|
import UrlUtils from "~/utils/app/UrlUtils";
|
|
import ApiUtils from "~/utils/app/ApiUtils";
|
|
import SimpleBadge from "~/components/ui/badges/SimpleBadge";
|
|
import { Colors } from "~/application/enums/shared/Colors";
|
|
import ApiCallSpeedBadge from "./ApiCallSpeedBadge";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
interface Props {
|
|
data: {
|
|
items: ApiCallSummaryDto[];
|
|
allTenants: { id: string; name: string }[];
|
|
filterableProperties: FilterablePropertyDto[];
|
|
};
|
|
}
|
|
export default function ApiKeyLogsSummary({ data }: Props) {
|
|
const { t } = useTranslation();
|
|
const formatters = useLocaleFormatters();
|
|
const params = useParams();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [groupBy, setGroupBy] = useState<string[]>(ApiKeyLogUtils.getGroupByValues(searchParams));
|
|
|
|
const [headers, setHeaders] = useState<RowHeaderDisplayDto<ApiCallSummaryDto>[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (ApiKeyLogUtils.getGroupByValues(searchParams).sort().join(",") !== groupBy.sort().join(",")) {
|
|
searchParams.delete("groupBy");
|
|
groupBy.forEach((by) => {
|
|
searchParams.append("groupBy", by);
|
|
});
|
|
setSearchParams(searchParams);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [groupBy]);
|
|
|
|
useEffect(() => {
|
|
const headers: RowHeaderDisplayDto<ApiCallSummaryDto>[] = [];
|
|
let currentGroupBy = groupBy;
|
|
if (currentGroupBy.length === 0) {
|
|
currentGroupBy = ApiKeyLogsConstants.DEFAULT_GROUP_BY;
|
|
}
|
|
const getTenant = (item: ApiCallSummaryDto) => {
|
|
return data.allTenants.find((x) => x.id === item.tenantId);
|
|
};
|
|
const getCountLink = (item: ApiCallSummaryDto) => {
|
|
const searchParams = new URLSearchParams();
|
|
// searchParams.set("pageSize", "100");
|
|
currentGroupBy.forEach((groupBy) => {
|
|
if (groupBy === "method") {
|
|
searchParams.append("method", item.method);
|
|
} else if (groupBy === "endpoint") {
|
|
searchParams.append("endpoint", item.endpoint);
|
|
} else if (groupBy === "params") {
|
|
searchParams.append("params", item.params);
|
|
} else if (groupBy === "status") {
|
|
searchParams.append("status", item.status?.toString() ?? "{null}");
|
|
}
|
|
});
|
|
return UrlUtils.getModulePath(params, `api/logs?${searchParams.toString()}`);
|
|
};
|
|
if (groupBy.includes("method")) {
|
|
headers.push({
|
|
name: "method",
|
|
title: t("models.apiKeyLog.method"),
|
|
value: (item) => (
|
|
<FilterableValueLink name="method" value={item.method}>
|
|
<SimpleBadge title={item.method} color={ApiUtils.getMethodColor(item.method)} underline />
|
|
</FilterableValueLink>
|
|
),
|
|
});
|
|
}
|
|
if (groupBy.includes("endpoint")) {
|
|
headers.push({
|
|
name: "endpoint",
|
|
title: t("models.apiKeyLog.endpoint"),
|
|
value: (item) => <FilterableValueLink name="endpoint" value={item.endpoint} />,
|
|
});
|
|
}
|
|
if (groupBy.includes("params")) {
|
|
headers.push({
|
|
name: "params",
|
|
title: t("models.apiKeyLog.params"),
|
|
value: (item) => <FilterableValueLink name="params" value={item.params} />,
|
|
});
|
|
}
|
|
if (groupBy.includes("status")) {
|
|
headers.push({
|
|
name: "status",
|
|
title: t("models.apiKeyLog.status"),
|
|
value: (item) => (
|
|
<FilterableValueLink name="status" value={item.status?.toString() ?? "{null}"}>
|
|
<SimpleBadge title={item.status?.toString() ?? t("shared.unknown")} color={item.status?.toString().startsWith("4") ? Colors.RED : Colors.GREEN} />
|
|
</FilterableValueLink>
|
|
),
|
|
});
|
|
}
|
|
if (groupBy.includes("tenantId")) {
|
|
headers.push({
|
|
name: "tenantId",
|
|
title: t("models.apiKeyLog.tenant"),
|
|
value: (item) => <FilterableValueLink name="tenantId" value={getTenant(item)?.name ?? ""} />,
|
|
});
|
|
}
|
|
headers.push({
|
|
name: "apiCalls",
|
|
title: t("models.apiKeyLog.apiCalls"),
|
|
align: "right",
|
|
value: (item) => (
|
|
<div className="flex justify-end text-right">
|
|
<Link to={getCountLink(item)} className="hover:underline">
|
|
{t("models.apiKeyLog.calls", { count: Number(item._count._all), formattedCount: formatters.number(Number(item._count._all)) })}
|
|
</Link>
|
|
</div>
|
|
),
|
|
});
|
|
|
|
headers.push({
|
|
name: "duration",
|
|
title: t("models.apiKeyLog.avgDuration"),
|
|
value: (item) =>
|
|
item._avg.duration === null ? (
|
|
<span className="text-muted-foreground text-xs italic">{t("shared.notAvailable")}</span>
|
|
) : (
|
|
<div>{formatters.number(Number(item._avg.duration), { style: "unit", unit: "millisecond", unitDisplay: "short", maximumFractionDigits: 3 })}</div>
|
|
),
|
|
});
|
|
headers.push({
|
|
name: "speed",
|
|
title: t("models.apiKeyLog.speed"),
|
|
value: (item) =>
|
|
item._avg.duration === null ? (
|
|
<span className="text-muted-foreground text-xs italic">{t("shared.notAvailable")}</span>
|
|
) : (
|
|
<ApiCallSpeedBadge duration={Number(item._avg.duration)} />
|
|
),
|
|
});
|
|
headers.push({
|
|
name: "actions",
|
|
title: "",
|
|
value: (item) => (
|
|
<Link to={getCountLink(item)} className="hover:underline">
|
|
{t("shared.details")} <span className="ml-1">→</span>
|
|
</Link>
|
|
),
|
|
});
|
|
|
|
setHeaders(headers);
|
|
}, [data, groupBy, params, t]);
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex w-full items-center space-x-2">
|
|
<InputCombobox
|
|
name="groupBy"
|
|
prefix={t("models.apiKeyLog.groupBy.prefix")}
|
|
className="bg-background h-8"
|
|
selectPlaceholder={t("models.apiKeyLog.groupBy.placeholder")}
|
|
options={[
|
|
{ value: "method", name: t("models.apiKeyLog.method") },
|
|
{ value: "endpoint", name: t("models.apiKeyLog.endpoint") },
|
|
{ value: "params", name: t("models.apiKeyLog.params") },
|
|
{ value: "status", name: t("models.apiKeyLog.status") },
|
|
{ value: "tenantId", name: t("models.apiKeyLog.tenant") },
|
|
]}
|
|
value={groupBy}
|
|
onChange={(value) => {
|
|
setGroupBy(value as string[]);
|
|
}}
|
|
/>
|
|
<div className="grow">{/* <InputSearchWithURL /> */}</div>
|
|
<InputFilters size="sm" filters={data.filterableProperties} />
|
|
</div>
|
|
{groupBy.length === 0 ? (
|
|
<WarningBanner title={t("models.apiKeyLog.groupBy.title")} text={t("models.apiKeyLog.groupBy.required")} />
|
|
) : (
|
|
<TableSimple items={data.items} headers={headers} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|