Add snapshots

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-08-19 14:28:13 +02:00
parent 9a7add8577
commit e12d04c947
49 changed files with 5560 additions and 185 deletions

View File

@@ -0,0 +1,45 @@
import { IconClock } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useLazyLoadQuery, graphql } from "react-relay";
import type { SnapshotBannerQuery } from "./__generated__/SnapshotBannerQuery.graphql";
const snapshotQuery = graphql`
query SnapshotBannerQuery($snapshotId: ID!) {
node(id: $snapshotId) {
... on Snapshot {
id
name
createdAt
}
}
}
`;
type Props = {
snapshotId: string;
};
export function SnapshotBanner({ snapshotId }: Props) {
const { __, dateFormat } = useTranslate();
const data = useLazyLoadQuery<SnapshotBannerQuery>(snapshotQuery, { snapshotId });
const snapshot = data.node;
if (!snapshot) {
return null;
}
return (
<div className="bg-warning rounded-lg p-4 flex items-center gap-3">
<IconClock className="text-warning-600 flex-shrink-0" size={20} />
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-warning-800">{__("Snapshot")} {snapshot.name}</span>
</div>
<p className="text-sm text-warning-700">
{__("You are viewing a snapshot of data from")} {dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })}
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,144 @@
/**
* @generated SignedSource<<a11c48ca33ac17dd7eaf6eeca4a0c20d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SnapshotBannerQuery$variables = {
snapshotId: string;
};
export type SnapshotBannerQuery$data = {
readonly node: {
readonly createdAt?: any;
readonly id?: string;
readonly name?: string;
};
};
export type SnapshotBannerQuery = {
response: SnapshotBannerQuery$data;
variables: SnapshotBannerQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "snapshotId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "snapshotId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SnapshotBannerQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/)
],
"type": "Snapshot",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SnapshotBannerQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/)
],
"type": "Snapshot",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "8f21371ececf96444bb6b00e52783573",
"id": null,
"metadata": {},
"name": "SnapshotBannerQuery",
"operationKind": "query",
"text": "query SnapshotBannerQuery(\n $snapshotId: ID!\n) {\n node(id: $snapshotId) {\n __typename\n ... on Snapshot {\n id\n name\n createdAt\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "1602d5b6c0ae4a3122998e4d3419af05";
export default node;

View File

@@ -0,0 +1,17 @@
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui";
import { snapshotTypes, getSnapshotTypeLabel } from "@probo/helpers";
export function SnapshotTypeOptions() {
const { __ } = useTranslate();
return (
<>
{snapshotTypes.map((type) => (
<Option key={type} value={type}>
{getSnapshotTypeLabel(__, type)}
</Option>
))}
</>
);
}

View File

@@ -1,23 +1,32 @@
import { Avatar, Field, Option, Select, Badge, Button, IconCrossLargeX } from "@probo/ui";
import { Suspense, useState, type ComponentProps } from "react";
import { useTranslate } from "@probo/i18n";
import { type Control, Controller } from "react-hook-form";
import { type Control, Controller, type FieldValues } from "react-hook-form";
import { useVendors } from "/hooks/graph/VendorGraph.ts";
import { faviconUrl } from "@probo/helpers";
import type { Path } from "react-hook-form";
type Props = {
type Vendor = {
id: string;
name: string;
websiteUrl: string | null | undefined;
};
type Props<T extends FieldValues = FieldValues> = {
organizationId: string;
control: Control<any>;
control: Control<T>;
name: string;
label?: string;
error?: string;
selectedVendors?: Vendor[];
} & ComponentProps<typeof Field>;
export function VendorsMultiSelectField({
export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
organizationId,
control,
selectedVendors = [],
...props
}: Props) {
}: Props<T>) {
return (
<Field {...props}>
<Suspense
@@ -28,29 +37,40 @@ export function VendorsMultiSelectField({
control={control}
name={props.name}
disabled={props.disabled}
selectedVendors={selectedVendors}
/>
</Suspense>
</Field>
);
}
function VendorsMultiSelectWithQuery(
props: Pick<Props, "organizationId" | "control" | "name" | "disabled">
function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">
) {
const { __ } = useTranslate();
const { name, organizationId, control } = props;
const { name, organizationId, control, selectedVendors = [] } = props;
const vendors = useVendors(organizationId);
const [isOpen, setIsOpen] = useState(false);
const allVendors = [...vendors];
if (props.disabled) {
selectedVendors.forEach(selectedVendor => {
if (!allVendors.find(v => v.id === selectedVendor.id)) {
allVendors.push(selectedVendor);
}
});
}
return (
<>
<Controller
control={control}
name={name}
name={name as Path<T>}
render={({ field }) => {
const selectedVendorIds = Array.isArray(field.value) ? field.value : [];
const selectedVendors = vendors.filter(v => selectedVendorIds.includes(v.id));
const availableVendors = vendors.filter(v => !selectedVendorIds.includes(v.id));
const selectedVendorIds = (Array.isArray(field.value) ? field.value : []) as string[];
const selectedVendors = allVendors.filter(v => selectedVendorIds.includes(v.id));
const availableVendors = allVendors.filter(v => !selectedVendorIds.includes(v.id));
const handleAddVendor = (vendorId: string) => {
const newValue = [...selectedVendorIds, vendorId];
@@ -65,7 +85,7 @@ function VendorsMultiSelectWithQuery(
return (
<div className="space-y-2">
{availableVendors.length > 0 && (
{availableVendors.length > 0 && !props.disabled && (
<Select
disabled={props.disabled}
id={name}
@@ -108,12 +128,14 @@ function VendorsMultiSelectWithQuery(
size="s"
/>
<span>{vendor.name}</span>
<Button
variant="tertiary"
icon={IconCrossLargeX}
onClick={() => handleRemoveVendor(vendor.id)}
className="h-4 w-4 p-0 hover:bg-transparent"
/>
{!props.disabled && (
<Button
variant="tertiary"
icon={IconCrossLargeX}
onClick={() => handleRemoveVendor(vendor.id)}
className="h-4 w-4 p-0 hover:bg-transparent"
/>
)}
</Badge>
))}
</div>