Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,134 @@
import { useSubmit, useNavigation, useParams, useSearchParams, Link } from "react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { FilterablePropertyDto } from "~/application/dtos/data/FilterablePropertyDto";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
import RefreshIcon from "~/components/ui/icons/RefreshIcon";
import InputFilters from "~/components/ui/input/InputFilters";
import InputSearchWithURL from "~/components/ui/input/InputSearchWithURL";
import { FilterableValueLink } from "~/components/ui/links/FilterableValueLink";
import TableSimple from "~/components/ui/tables/TableSimple";
import { useLocaleFormatters } from "~/locale/format";
import { CreditWithDetails } from "../db/credits.db.server";
import { CreditTypes } from "../dtos/CreditType";
interface Props {
data: {
items: CreditWithDetails[];
filterableProperties: FilterablePropertyDto[];
pagination: PaginationDto;
canDelete: boolean;
};
}
export default function CreditsList({ data }: Props) {
const { t } = useTranslation();
const formatters = useLocaleFormatters();
const submit = useSubmit();
const navigation = useNavigation();
const params = useParams();
const canDelete = data.canDelete;
const [searchParams, setSearchParams] = useSearchParams();
const [selectedRows, setSelectedRows] = useState<CreditWithDetails[]>([]);
function onDelete(ids: string[]) {
if (!canDelete) {
return;
}
const form = new FormData();
form.set("action", "delete");
form.set("ids", ids.join(","));
submit(form, {
method: "post",
});
}
return (
<div className="space-y-2">
<div className="flex w-full items-center space-x-2">
<div className="grow">
<InputSearchWithURL />
</div>
{canDelete && selectedRows.length > 0 && (
<ButtonSecondary
destructive
onClick={() => {
onDelete(selectedRows.map((x) => x.id));
setSelectedRows([]);
}}
>
{t("shared.delete")} {selectedRows.length}
</ButtonSecondary>
)}
<ButtonSecondary onClick={() => setSearchParams(searchParams)} isLoading={navigation.state === "loading"}>
<RefreshIcon className="h-4 w-4" />
</ButtonSecondary>
<InputFilters filters={data.filterableProperties} />
</div>
<TableSimple
selectedRows={canDelete ? selectedRows : undefined}
onSelected={canDelete ? setSelectedRows : undefined}
items={data.items}
pagination={data.pagination}
actions={[
{
title: t("shared.delete"),
onClick: (_, item) => onDelete([item.id]),
hidden: () => !canDelete,
disabled: () => !canDelete,
destructive: true,
},
]}
headers={[
{
name: "createdAt",
title: t("shared.createdAt"),
value: (item) => formatters.dateTime(item.createdAt, { timeStyle: "medium" }),
formattedValue: (item) => (
<div className="text-muted-foreground text-xs">
{item.createdAt && <span>{formatters.dateTime(item.createdAt, { timeStyle: "medium" })}</span>}
</div>
),
},
{
name: "tenant",
title: t("models.tenant.object"),
value: (item) => <FilterableValueLink name="tenantId" value={item?.tenant?.name} param={item?.tenant?.id} />,
hidden: !!params.tenant,
},
{
name: "type",
title: t("shared.type"),
value: (item) => {
const creditType = CreditTypes.find((type) => type.value === item.type);
return <div>{creditType ? t(creditType.name) : t("models.credit.types.unknown", { type: item.type })}</div>;
},
},
{
name: "resource",
title: t("models.credit.resource"),
className: "w-full",
value: (item) => (
<div className="max-w-xs truncate">
{item.objectId ? (
<Link to={item.objectId} className="truncate underline">
{item.objectId}
</Link>
) : (
<span className="truncate">{t("shared.notSet")}</span>
)}
</div>
),
},
{
name: "user",
title: t("models.user.object"),
value: (item) => <FilterableValueLink name="userId" value={item.user?.email} param={item.user?.id} />,
},
]}
/>
</div>
);
}
@@ -0,0 +1,27 @@
import { Link } from "react-router";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { useLocaleFormatters } from "~/locale/format";
export default function CreditsRemaining({ remaining, redirectTo }: { remaining?: number | "unlimited"; redirectTo: string }) {
const { t } = useTranslation();
const formatters = useLocaleFormatters();
return (
<Fragment>
{!!remaining && (
<Link
to={redirectTo}
className="cursor-pointer select-none rounded-full border border-green-200 bg-green-50 px-2 py-1 text-xs text-green-700 hover:bg-green-100"
>
{remaining === "unlimited" ? (
<span>{t("models.credit.unlimited")}</span>
) : remaining <= 0 ? (
<span>{t("models.credit.empty")}</span>
) : (
<span>{t("models.credit.remaining", { 0: formatters.number(remaining) })}</span>
)}
</Link>
)}
</Fragment>
);
}
@@ -0,0 +1,39 @@
import TableSimple from "~/components/ui/tables/TableSimple";
import { CreditTypes } from "../dtos/CreditType";
import { useTranslation } from "react-i18next";
import { useLocaleFormatters } from "~/locale/format";
export default function CreditsTableInfo() {
const { t } = useTranslation();
const formatters = useLocaleFormatters();
return (
<div className="space-y-3">
<div className="space-y-2">
<h3 className="text-lg font-medium">{t("models.credit.whatIs")}</h3>
<p className="text-muted-foreground text-sm">{t("models.credit.info")}</p>
</div>
<TableSimple
items={CreditTypes}
headers={[
{
name: "name",
title: t("shared.name"),
className: "w-full",
value: (item) => (
<div className="flex flex-col">
<div className="font-bold">{t(item.name)}</div>
<div className="text-muted-foreground text-xs">{t(item.description)}</div>
</div>
),
},
{
name: "amount",
title: t("models.credit.plural"),
align: "right",
value: (item) => formatters.number(item.amount),
},
]}
/>
</div>
);
}
@@ -0,0 +1,36 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
function readUsageComponent(fileName: string) {
return fs.readFileSync(path.join(root, "apps/web/app/modules/usage/components", fileName), "utf8");
}
describe("usage credit formatting i18n", () => {
it("uses locale-aware formatters for credit list timestamps", () => {
const source = readUsageComponent("CreditsList.tsx");
expect(source).not.toContain("DateUtils");
expect(source).not.toContain('title: "Tenant"');
expect(source).toContain("useLocaleFormatters");
expect(source).toContain('t("models.tenant.object")');
expect(source).toContain('t("shared.notSet")');
expect(source).toContain('t(creditType.name)');
expect(source).toContain('t("models.credit.types.unknown"');
expect(source).toContain("formatters.dateTime");
expect(source).not.toContain('t("shared.undefined")');
expect(source).not.toContain("<div>{item.type}</div>");
});
it("uses locale-aware formatters for credit amounts", () => {
const sources = ["CreditsRemaining.tsx", "CreditsTableInfo.tsx"].map(readUsageComponent).join("\n");
expect(sources).not.toContain("NumberUtils");
expect(sources).toContain("useLocaleFormatters");
expect(sources).toContain("formatters.number");
expect(sources).toContain("t(item.name)");
expect(sources).toContain("t(item.description)");
});
});