Add vendor snapshots

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-01 10:24:17 +02:00
parent d7ffed9d7f
commit 078102db74
32 changed files with 1381 additions and 426 deletions

View File

@@ -192,6 +192,7 @@ export default function AssetDetailsPage(props: Props) {
organizationId={organizationId}
control={control}
name="vendorIds"
selectedVendors={vendors}
label={__("Vendors")}
disabled={isSnapshotMode}
/>

View File

@@ -23,11 +23,12 @@ import {
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Outlet } from "react-router";
import { faviconUrl } from "@probo/helpers";
import { Outlet, useParams } from "react-router";
import { faviconUrl, validateSnapshotConsistency } from "@probo/helpers";
import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Props = {
queryRef: PreloadedQuery<VendorGraphNodeQuery>;
@@ -38,6 +39,14 @@ export default function VendorDetailPage(props: Props) {
const vendor = data.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
if (!vendor) {
return <div>{__("Vendor not found")}</div>;
}
validateSnapshotConsistency(vendor, snapshotId);
const deleteVendor = useDeleteVendor(
vendor,
ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey)
@@ -48,13 +57,22 @@ export default function VendorDetailPage(props: Props) {
vendor as VendorComplianceTabFragment$key
).complianceReports.edges.length;
const vendorsUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors`
: `/organizations/${organizationId}/vendors`;
const baseVendorUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}`
: `/organizations/${organizationId}/vendors/${vendor.id}`;
return (
<div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<Breadcrumb
items={[
{
label: __("Vendors"),
to: `/organizations/${organizationId}/vendors`,
to: vendorsUrl,
},
{
label: vendor.name ?? "",
@@ -72,54 +90,44 @@ export default function VendorDetailPage(props: Props) {
)}
<div className="text-2xl">{vendor.name}</div>
</div>
<div className="flex gap-2 items-center">
<ImportAssessmentDialog vendorId={vendor.id!}>
<Button icon={IconPageTextLine} variant="secondary">
{__("Assessment From Website")}
</Button>
</ImportAssessmentDialog>
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
icon={IconTrashCan}
onClick={deleteVendor}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</div>
{!isSnapshotMode && (
<div className="flex gap-2 items-center">
<ImportAssessmentDialog vendorId={vendor.id!}>
<Button icon={IconPageTextLine} variant="secondary">
{__("Assessment From Website")}
</Button>
</ImportAssessmentDialog>
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
icon={IconTrashCan}
onClick={deleteVendor}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</div>
)}
</div>
<Tabs>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}
>
<TabLink to={`${baseVendorUrl}/overview`}>
{__("Overview")}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/certifications`}
>
<TabLink to={`${baseVendorUrl}/certifications`}>
{__("Certifications")}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/compliance`}
>
<TabLink to={`${baseVendorUrl}/compliance`}>
{__("Compliance reports")}
{reportsCount > 0 && <TabBadge>{reportsCount}</TabBadge>}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/risks`}
>
<TabLink to={`${baseVendorUrl}/risks`}>
{__("Risk Assessment")}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/contacts`}
>
<TabLink to={`${baseVendorUrl}/contacts`}>
{__("Contacts")}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/vendors/${vendor.id}/services`}
>
<TabLink to={`${baseVendorUrl}/services`}>
{__("Services")}
</TabLink>
</Tabs>

View File

@@ -21,6 +21,7 @@ import {
type PreloadedQuery,
} from "react-relay";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useParams } from "react-router";
import { faviconUrl } from "@probo/helpers";
import type { NodeOf } from "/types";
import { CreateVendorDialog } from "./dialogs/CreateVendorDialog";
@@ -35,6 +36,7 @@ import type {
VendorGraphPaginatedFragment$key,
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
import { SortableTable, SortableTh } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner";
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
@@ -45,6 +47,8 @@ type Props = {
export default function VendorsPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const data = usePreloadedQuery(vendorsQuery, props.queryRef);
const pagination = usePaginationFragment(
@@ -59,18 +63,21 @@ export default function VendorsPage(props: Props) {
return (
<div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<PageHeader
title={__("Vendors")}
description={__(
"Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status."
)}
>
<CreateVendorDialog
connection={connectionId}
organizationId={organizationId}
>
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
</CreateVendorDialog>
{!isSnapshotMode && (
<CreateVendorDialog
connection={connectionId}
organizationId={organizationId}
>
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
</CreateVendorDialog>
)}
</PageHeader>
<SortableTable {...pagination}>
<Thead>
@@ -106,14 +113,20 @@ function VendorRow({
organizationId: string;
connectionId: string;
}) {
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { __, dateFormat } = useTranslate();
const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
const deleteVendor = useDeleteVendor(vendor, connectionId);
const vendorUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/vendors/${vendor.id}/overview`
: `/organizations/${organizationId}/vendors/${vendor.id}/overview`;
return (
<>
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}>
<Tr to={vendorUrl}>
<Td>
<div className="flex gap-2 items-center">
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} />
@@ -136,15 +149,17 @@ function VendorRow({
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
</Td>
<Td noLink width={50} className="text-end">
<ActionDropdown>
<DropdownItem
onClick={deleteVendor}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
{!isSnapshotMode && (
<ActionDropdown>
<DropdownItem
onClick={deleteVendor}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
)}
</Td>
</Tr>
</>

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router";
import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql";
import { useTranslate } from "@probo/i18n";
@@ -98,6 +98,8 @@ export default function VendorComplianceTab() {
const connectionId = data.complianceReports.__id;
const reports = data.complianceReports.edges.map((edge) => edge.node);
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
usePageTitle(vendor.name + " - " + __("Compliance reports"));
@@ -125,15 +127,17 @@ export default function VendorComplianceTab() {
return (
<div className="space-y-6">
<Dropzone
description={__("Only PDF files up to 10MB are allowed")}
isUploading={isMutating}
onDrop={handleDrop}
accept={{
"application/pdf": [".pdf"],
}}
maxSize={10}
/>
{!isSnapshotMode && (
<Dropzone
description={__("Only PDF files up to 10MB are allowed")}
isUploading={isMutating}
onDrop={handleDrop}
accept={{
"application/pdf": [".pdf"],
}}
maxSize={10}
/>
)}
<SortableTable refetch={refetch}>
<Thead>
<Tr>
@@ -141,7 +145,7 @@ export default function VendorComplianceTab() {
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
<Th>{__("Valid until")}</Th>
<Th>{__("File size")}</Th>
<Th>{__("Actions")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr>
</Thead>
<Tbody>
@@ -150,6 +154,7 @@ export default function VendorComplianceTab() {
key={report.id}
reportKey={report}
connectionId={connectionId}
isSnapshotMode={isSnapshotMode}
/>
))}
</Tbody>
@@ -161,6 +166,7 @@ export default function VendorComplianceTab() {
type ReportRowProps = {
reportKey: VendorComplianceTabFragment_report$key;
connectionId: string;
isSnapshotMode: boolean;
};
function ReportRow(props: ReportRowProps) {
@@ -204,17 +210,19 @@ function ReportRow(props: ReportRowProps) {
<Td>{dateFormat(report.reportDate)}</Td>
<Td>{dateFormat(report.validUntil)}</Td>
<Td>{fileSize(__, report.fileSize)}</Td>
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
)}
</Tr>
);
}

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router";
import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { VendorContactsTabFragment$key } from "./__generated__/VendorContactsTabFragment.graphql";
import { useTranslate } from "@probo/i18n";
@@ -89,6 +89,8 @@ export default function VendorContactsTab() {
const connectionId = data.contacts.__id;
const contacts = data.contacts.edges.map((edge) => edge.node);
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [editingContact, setEditingContact] = useState<{
id: string;
fullName?: string | null;
@@ -105,12 +107,14 @@ export default function VendorContactsTab() {
title={__("Contacts")}
description={__("Manage vendor contacts and their information.")}
>
<CreateContactDialog
vendorId={vendor.id}
connectionId={connectionId}
>
<Button icon={IconPlusLarge}>{__("Add contact")}</Button>
</CreateContactDialog>
{!isSnapshotMode && (
<CreateContactDialog
vendorId={vendor.id}
connectionId={connectionId}
>
<Button icon={IconPlusLarge}>{__("Add contact")}</Button>
</CreateContactDialog>
)}
</PageHeader>
<SortableTable refetch={refetch}>
@@ -120,7 +124,7 @@ export default function VendorContactsTab() {
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
<Th>{__("Phone")}</Th>
<Th>{__("Role")}</Th>
<Th>{__("Actions")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr>
</Thead>
<Tbody>
@@ -130,12 +134,13 @@ export default function VendorContactsTab() {
contactKey={contact}
connectionId={connectionId}
onEdit={setEditingContact}
isSnapshotMode={isSnapshotMode}
/>
))}
</Tbody>
</SortableTable>
{editingContact && (
{editingContact && !isSnapshotMode && (
<EditContactDialog
contactId={editingContact.id}
contact={editingContact}
@@ -156,6 +161,7 @@ type ContactRowProps = {
phone?: string | null;
role?: string | null;
}) => void;
isSnapshotMode: boolean;
};
function ContactRow(props: ContactRowProps) {
@@ -220,29 +226,31 @@ function ContactRow(props: ContactRowProps) {
)}
</Td>
<Td>{contact.role || __("—")}</Td>
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconPencil}
onClick={() => props.onEdit({
id: contact.id,
fullName: contact.fullName,
email: contact.email,
phone: contact.phone,
role: contact.role,
})}
>
{__("Edit")}
</DropdownItem>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconPencil}
onClick={() => props.onEdit({
id: contact.id,
fullName: contact.fullName,
email: contact.email,
phone: contact.phone,
role: contact.role,
})}
>
{__("Edit")}
</DropdownItem>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
)}
</Tr>
);
}

View File

@@ -1,6 +1,6 @@
import { useTranslate } from "@probo/i18n";
import { useVendorForm } from "/hooks/forms/useVendorForm";
import { useOutletContext } from "react-router";
import { useOutletContext, useParams } from "react-router";
import { Button, Card, Field, Input, IconPlusLarge, IconTrashCan, IconPencil } from "@probo/ui";
import { PeopleSelectField } from "/components/form/PeopleSelectField";
import { useOrganizationId } from "/hooks/useOrganizationId";
@@ -55,6 +55,8 @@ export default function VendorOverviewTab() {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const {
control,
@@ -98,7 +100,7 @@ export default function VendorOverviewTab() {
usePageTitle(vendor.name + " - " + __("Overview"));
return (
<form onSubmit={handleSubmit} className="space-y-12">
<form onSubmit={isSnapshotMode ? undefined : handleSubmit} className="space-y-12">
{/* Vendor Details */}
<div className="space-y-4">
<h2 className="text-base font-medium">{__("Vendor details")}</h2>
@@ -108,30 +110,35 @@ export default function VendorOverviewTab() {
label={__("Name")}
type="text"
error={errors.name?.message}
disabled={isSubmitting || isSnapshotMode}
/>
<Field
{...register("description")}
label={__("Description")}
type="textarea"
error={errors.description?.message}
disabled={isSubmitting || isSnapshotMode}
/>
<Field
{...register("legalName")}
label={__("Legal name")}
type="text"
error={errors.legalName?.message}
disabled={isSubmitting || isSnapshotMode}
/>
<Field
{...register("headquarterAddress")}
label={__("Headquarter address")}
type="textarea"
error={errors.headquarterAddress?.message}
disabled={isSubmitting || isSnapshotMode}
/>
<Field
{...register("websiteUrl")}
label={__("Website URL")}
type="text"
error={errors.websiteUrl?.message}
disabled={isSubmitting || isSnapshotMode}
/>
</Card>
</div>
@@ -146,6 +153,7 @@ export default function VendorOverviewTab() {
name="businessOwnerId"
label={__("Business owner")}
error={errors.businessOwnerId?.message}
disabled={isSubmitting || isSnapshotMode}
/>
<PeopleSelectField
organizationId={organizationId}
@@ -153,6 +161,7 @@ export default function VendorOverviewTab() {
name="securityOwnerId"
label={__("Security owner")}
error={errors.securityOwnerId?.message}
disabled={isSubmitting || isSnapshotMode}
/>
</Card>
</div>
@@ -180,6 +189,7 @@ export default function VendorOverviewTab() {
type="text"
placeholder="https://..."
variant="ghost"
disabled={isSubmitting || isSnapshotMode}
/>
</div>
))}
@@ -220,33 +230,39 @@ export default function VendorOverviewTab() {
>
{__("Download PDF")}
</Button>
<EditBusinessAssociateAgreementDialog
vendorId={vendor.id}
agreement={{
validFrom: businessAssociateAgreement.validFrom,
validUntil: businessAssociateAgreement.validUntil,
}}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconPencil} />
</EditBusinessAssociateAgreementDialog>
<DeleteBusinessAssociateAgreementDialog
vendorId={vendor.id}
fileName={businessAssociateAgreement.fileName}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconTrashCan} />
</DeleteBusinessAssociateAgreementDialog>
{!isSnapshotMode && (
<>
<EditBusinessAssociateAgreementDialog
vendorId={vendor.id}
agreement={{
validFrom: businessAssociateAgreement.validFrom,
validUntil: businessAssociateAgreement.validUntil,
}}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconPencil} />
</EditBusinessAssociateAgreementDialog>
<DeleteBusinessAssociateAgreementDialog
vendorId={vendor.id}
fileName={businessAssociateAgreement.fileName}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconTrashCan} />
</DeleteBusinessAssociateAgreementDialog>
</>
)}
</>
) : (
<UploadBusinessAssociateAgreementDialog
vendorId={vendor.id}
onSuccess={() => window.location.reload()}
>
<Button variant="secondary" icon={IconPlusLarge}>
{__("Upload")}
</Button>
</UploadBusinessAssociateAgreementDialog>
!isSnapshotMode && (
<UploadBusinessAssociateAgreementDialog
vendorId={vendor.id}
onSuccess={() => window.location.reload()}
>
<Button variant="secondary" icon={IconPlusLarge}>
{__("Upload")}
</Button>
</UploadBusinessAssociateAgreementDialog>
)
)}
</div>
</div>
@@ -281,26 +297,30 @@ export default function VendorOverviewTab() {
>
{__("Download PDF")}
</Button>
<EditDataPrivacyAgreementDialog
vendorId={vendor.id}
agreement={{
validFrom: dataPrivacyAgreement.validFrom,
validUntil: dataPrivacyAgreement.validUntil,
}}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconPencil} />
</EditDataPrivacyAgreementDialog>
<DeleteDataPrivacyAgreementDialog
vendorId={vendor.id}
fileName={dataPrivacyAgreement.fileName}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconTrashCan} />
</DeleteDataPrivacyAgreementDialog>
{!isSnapshotMode && (
<>
<EditDataPrivacyAgreementDialog
vendorId={vendor.id}
agreement={{
validFrom: dataPrivacyAgreement.validFrom,
validUntil: dataPrivacyAgreement.validUntil,
}}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconPencil} />
</EditDataPrivacyAgreementDialog>
<DeleteDataPrivacyAgreementDialog
vendorId={vendor.id}
fileName={dataPrivacyAgreement.fileName}
onSuccess={() => window.location.reload()}
>
<Button variant="quaternary" icon={IconTrashCan} />
</DeleteDataPrivacyAgreementDialog>
</>
)}
</>
) : (
<>
!isSnapshotMode && (
<UploadDataPrivacyAgreementDialog
vendorId={vendor.id}
onSuccess={() => window.location.reload()}
@@ -309,7 +329,7 @@ export default function VendorOverviewTab() {
{__("Upload")}
</Button>
</UploadDataPrivacyAgreementDialog>
</>
)
)}
</div>
</div>
@@ -317,11 +337,13 @@ export default function VendorOverviewTab() {
</div>
{/* Submit */}
<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}>
{__("Update vendor")}
</Button>
</div>
{!isSnapshotMode && (
<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}>
{__("Update vendor")}
</Button>
</div>
)}
</form>
);
}

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router";
import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { VendorRiskAssessmentTabFragment$key } from "./__generated__/VendorRiskAssessmentTabFragment.graphql";
import { useTranslate } from "@probo/i18n";
@@ -82,6 +82,8 @@ export default function VendorRiskAssessmentTab() {
);
const assessments = data.riskAssessments.edges.map((edge) => edge.node);
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [expanded, setExpanded] = useState<string | null>(null);
usePageTitle(vendor.name + " - " + __("Risk Assessments"));
@@ -90,15 +92,17 @@ export default function VendorRiskAssessmentTab() {
return (
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
{__("No risk assessments found")}
<CreateRiskAssessmentDialog
vendorId={vendor.id}
connection={data.riskAssessments.__id}
peopleId={peopleId}
>
<Button icon={IconPlusLarge} variant="secondary">
{__("Add Risk Assessment")}
</Button>
</CreateRiskAssessmentDialog>
{!isSnapshotMode && (
<CreateRiskAssessmentDialog
vendorId={vendor.id}
connection={data.riskAssessments.__id}
peopleId={peopleId}
>
<Button icon={IconPlusLarge} variant="secondary">
{__("Add Risk Assessment")}
</Button>
</CreateRiskAssessmentDialog>
)}
</div>
);
}
@@ -117,15 +121,17 @@ export default function VendorRiskAssessmentTab() {
</Tr>
</Thead>
<Tbody>
<CreateRiskAssessmentDialog
vendorId={vendor.id}
connection={data.riskAssessments.__id}
peopleId={peopleId}
>
<TrButton colspan={5} onClick={() => {}}>
{__("Add Risk Assessment")}
</TrButton>
</CreateRiskAssessmentDialog>
{!isSnapshotMode && (
<CreateRiskAssessmentDialog
vendorId={vendor.id}
connection={data.riskAssessments.__id}
peopleId={peopleId}
>
<TrButton colspan={5} onClick={() => {}}>
{__("Add Risk Assessment")}
</TrButton>
</CreateRiskAssessmentDialog>
)}
{assessments.map((assessment) => (
<AssessmentRow
key={assessment.id}

View File

@@ -1,4 +1,4 @@
import { useOutletContext } from "react-router";
import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { VendorServicesTabFragment$key } from "./__generated__/VendorServicesTabFragment.graphql";
import { useTranslate } from "@probo/i18n";
@@ -87,6 +87,8 @@ export default function VendorServicesTab() {
const connectionId = data.services.__id;
const services = data.services.edges.map((edge) => edge.node);
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const [editingService, setEditingService] = useState<{
id: string;
name: string;
@@ -101,12 +103,14 @@ export default function VendorServicesTab() {
title={__("Services")}
description={__("Manage services provided by this vendor.")}
>
<CreateServiceDialog
vendorId={vendor.id}
connectionId={connectionId}
>
<Button icon={IconPlusLarge}>{__("Add service")}</Button>
</CreateServiceDialog>
{!isSnapshotMode && (
<CreateServiceDialog
vendorId={vendor.id}
connectionId={connectionId}
>
<Button icon={IconPlusLarge}>{__("Add service")}</Button>
</CreateServiceDialog>
)}
</PageHeader>
<SortableTable refetch={refetch}>
@@ -114,7 +118,7 @@ export default function VendorServicesTab() {
<Tr>
<SortableTh field="NAME">{__("Name")}</SortableTh>
<Th>{__("Description")}</Th>
<Th>{__("Actions")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
</Tr>
</Thead>
<Tbody>
@@ -124,12 +128,13 @@ export default function VendorServicesTab() {
serviceKey={service}
connectionId={connectionId}
onEdit={setEditingService}
isSnapshotMode={isSnapshotMode}
/>
))}
</Tbody>
</SortableTable>
{editingService && (
{editingService && !isSnapshotMode && (
<EditServiceDialog
serviceId={editingService.id}
service={editingService}
@@ -148,6 +153,7 @@ type ServiceRowProps = {
name: string;
description?: string | null;
}) => void;
isSnapshotMode: boolean;
};
function ServiceRow(props: ServiceRowProps) {
@@ -188,27 +194,29 @@ function ServiceRow(props: ServiceRowProps) {
<Tr>
<Td>{service.name}</Td>
<Td>{service.description || __("—")}</Td>
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconPencil}
onClick={() => props.onEdit({
id: service.id,
name: service.name,
description: service.description,
})}
>
{__("Edit")}
</DropdownItem>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
{!props.isSnapshotMode && (
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconPencil}
onClick={() => props.onEdit({
id: service.id,
name: service.name,
description: service.description,
})}
>
{__("Edit")}
</DropdownItem>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Td>
)}
</Tr>
);
}