45
apps/console/src/components/SnapshotBanner.tsx
Normal file
45
apps/console/src/components/SnapshotBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
144
apps/console/src/components/__generated__/SnapshotBannerQuery.graphql.ts
generated
Normal file
144
apps/console/src/components/__generated__/SnapshotBannerQuery.graphql.ts
generated
Normal 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;
|
||||
17
apps/console/src/components/form/SnapshotTypeOptions.tsx
Normal file
17
apps/console/src/components/form/SnapshotTypeOptions.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user