127
apps/console/src/components/ui/toast.tsx
Normal file
127
apps/console/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
33
apps/console/src/components/ui/toaster.tsx
Normal file
33
apps/console/src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
194
apps/console/src/hooks/use-toast.ts
Normal file
194
apps/console/src/hooks/use-toast.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
BreadcrumbProvider,
|
||||
useBreadcrumb,
|
||||
} from "@/contexts/BreadcrumbContext";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
function BreadcrumbNavigation() {
|
||||
const location = useLocation();
|
||||
@@ -90,6 +91,7 @@ export default function ConsoleLayout() {
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<Outlet />
|
||||
<Toaster />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HelpCircle, ArrowUpRight } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { HelpCircle, ArrowUpRight, Pencil, Check, X } from "lucide-react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
} from "react-relay";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Suspense, useEffect, useState, useCallback, useMemo } from "react";
|
||||
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
|
||||
import { useParams } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { UpdateVendorInput } from "./__generated__/VendorOverviewPageUpdateVendorMutation.graphql";
|
||||
|
||||
const vendorOverviewPageQuery = graphql`
|
||||
query VendorOverviewPageQuery($vendorId: ID!) {
|
||||
@@ -32,11 +37,75 @@ const vendorOverviewPageQuery = graphql`
|
||||
privacyPolicyUrl
|
||||
createdAt
|
||||
updatedAt
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const updateVendorMutation = graphql`
|
||||
mutation VendorOverviewPageUpdateVendorMutation($input: UpdateVendorInput!) {
|
||||
updateVendor(input: $input) {
|
||||
id
|
||||
name
|
||||
description
|
||||
serviceStartAt
|
||||
serviceTerminationAt
|
||||
serviceCriticality
|
||||
riskTier
|
||||
statusPageUrl
|
||||
termsOfServiceUrl
|
||||
privacyPolicyUrl
|
||||
updatedAt
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function EditableField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type = "text",
|
||||
helpText,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
type?: string;
|
||||
helpText?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">{label}</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Format date for input field (YYYY-MM-DDTHH:mm)
|
||||
function formatDateForInput(date: string | null | undefined): string {
|
||||
if (!date) return '';
|
||||
return new Date(date).toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Format date for API (2006-01-02T15:04:05.999999999Z07:00)
|
||||
function formatDateForAPI(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function VendorOverviewPageContent({
|
||||
queryRef,
|
||||
}: {
|
||||
@@ -44,8 +113,94 @@ function VendorOverviewPageContent({
|
||||
}) {
|
||||
const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef);
|
||||
const { setBreadcrumbSegment } = useBreadcrumb();
|
||||
const [selectedCriticality, setSelectedCriticality] = useState(data.node.serviceCriticality);
|
||||
const [selectedRiskTier, setSelectedRiskTier] = useState(data.node.riskTier);
|
||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
||||
const [formData, setFormData] = useState({
|
||||
name: data.node.name || '',
|
||||
description: data.node.description || '',
|
||||
// Format dates properly for datetime-local input
|
||||
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
||||
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
||||
serviceCriticality: data.node.serviceCriticality,
|
||||
riskTier: data.node.riskTier,
|
||||
statusPageUrl: data.node.statusPageUrl || '',
|
||||
termsOfServiceUrl: data.node.termsOfServiceUrl || '',
|
||||
privacyPolicyUrl: data.node.privacyPolicyUrl || '',
|
||||
});
|
||||
const [commit] = useMutation(updateVendorMutation);
|
||||
const [_, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(vendorOverviewPageQuery);
|
||||
const { toast } = useToast();
|
||||
|
||||
const hasChanges = editedFields.size > 0;
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const formattedData = {
|
||||
...formData,
|
||||
serviceStartAt: formatDateForAPI(formData.serviceStartAt),
|
||||
serviceTerminationAt: formData.serviceTerminationAt ? formatDateForAPI(formData.serviceTerminationAt) : null,
|
||||
};
|
||||
|
||||
commit({
|
||||
variables: {
|
||||
input: {
|
||||
id: data.node.id,
|
||||
expectedVersion: data.node.version,
|
||||
...formattedData,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Changes saved successfully",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.message?.includes('concurrent modification')) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Someone else modified this vendor. Reloading latest data.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
loadQuery({ vendorId: data.node.id! });
|
||||
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to save changes",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
},
|
||||
updater: (store) => {
|
||||
// Clear any error states if needed
|
||||
},
|
||||
});
|
||||
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
||||
|
||||
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
setEditedFields(prev => new Set(prev).add(field));
|
||||
};
|
||||
|
||||
// Update the cancel handler to also format dates
|
||||
const handleCancel = () => {
|
||||
setFormData({
|
||||
name: data.node.name || '',
|
||||
description: data.node.description || '',
|
||||
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
||||
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
||||
serviceCriticality: data.node.serviceCriticality,
|
||||
riskTier: data.node.riskTier,
|
||||
statusPageUrl: data.node.statusPageUrl || '',
|
||||
termsOfServiceUrl: data.node.termsOfServiceUrl || '',
|
||||
privacyPolicyUrl: data.node.privacyPolicyUrl || '',
|
||||
});
|
||||
setEditedFields(new Set());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data.node?.name) {
|
||||
@@ -54,205 +209,185 @@ function VendorOverviewPageContent({
|
||||
}, [data.node?.name, setBreadcrumbSegment]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6 lg:p-8">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
{data.node.name}
|
||||
</h1>
|
||||
<p className="text-gray-600">{data.node.description}</p>
|
||||
</div>
|
||||
<>
|
||||
<div className="space-y-6 p-4 md:p-6 lg:p-8">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<EditableField
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(value) => handleFieldChange('name', value)}
|
||||
/>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Service Information</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Basic information about the vendor service
|
||||
</p>
|
||||
</div>
|
||||
<EditableField
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(value) => handleFieldChange('description', value)}
|
||||
/>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Service Start Date</Label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
{new Date(data.node.serviceStartAt).toLocaleDateString()}
|
||||
<h2 className="text-lg font-medium">Service Information</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Basic information about the vendor service
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{data.node?.serviceTerminationAt && (
|
||||
<div className="space-y-4">
|
||||
<EditableField
|
||||
label="Service Start At"
|
||||
value={formData.serviceStartAt}
|
||||
type="datetime-local"
|
||||
onChange={(value) => handleFieldChange('serviceStartAt', value)}
|
||||
/>
|
||||
|
||||
<EditableField
|
||||
label="Service Termination At"
|
||||
value={formData.serviceTerminationAt}
|
||||
type="datetime-local"
|
||||
onChange={(value) => handleFieldChange('serviceTerminationAt', value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Service Termination At</Label>
|
||||
<Label className="text-sm">Service Criticality</Label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
{new Date(data.node.serviceTerminationAt).toLocaleDateString()}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleFieldChange('serviceCriticality', 'LOW')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.serviceCriticality === 'LOW'
|
||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Low
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFieldChange('serviceCriticality', 'MEDIUM')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.serviceCriticality === 'MEDIUM'
|
||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Medium
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFieldChange('serviceCriticality', 'HIGH')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.serviceCriticality === 'HIGH'
|
||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
High
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{formData.serviceCriticality === 'HIGH' &&
|
||||
"Critical service - downtime severely impacts end-users"}
|
||||
{formData.serviceCriticality === 'MEDIUM' &&
|
||||
"Important service - downtime moderately affects end-users"}
|
||||
{formData.serviceCriticality === 'LOW' &&
|
||||
"Non-critical service - minimal end-user impact if down"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Service Criticality</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Risk Tier</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleFieldChange('riskTier', 'CRITICAL')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.riskTier === 'CRITICAL'
|
||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Critical
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFieldChange('riskTier', 'SIGNIFICANT')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.riskTier === 'SIGNIFICANT'
|
||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Significant
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFieldChange('riskTier', 'GENERAL')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.riskTier === 'GENERAL'
|
||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{formData.riskTier === 'CRITICAL' &&
|
||||
"Handles sensitive data, critical for platform operation"}
|
||||
{formData.riskTier === 'SIGNIFICANT' &&
|
||||
"No user data access, but important for platform management"}
|
||||
{formData.riskTier === 'GENERAL' &&
|
||||
"General vendor with minimal risk"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedCriticality('LOW')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedCriticality === 'LOW'
|
||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Low
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedCriticality('MEDIUM')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedCriticality === 'MEDIUM'
|
||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Medium
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedCriticality('HIGH')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedCriticality === 'HIGH'
|
||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
High
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{selectedCriticality === 'HIGH' &&
|
||||
"Critical service - downtime severely impacts end-users"}
|
||||
{selectedCriticality === 'MEDIUM' &&
|
||||
"Important service - downtime moderately affects end-users"}
|
||||
{selectedCriticality === 'LOW' &&
|
||||
"Non-critical service - minimal end-user impact if down"}
|
||||
</p>
|
||||
|
||||
<EditableField
|
||||
label="Status Page URL"
|
||||
value={formData.statusPageUrl || ""}
|
||||
onChange={(value) => handleFieldChange('statusPageUrl', value)}
|
||||
/>
|
||||
|
||||
<EditableField
|
||||
label="Terms of Service URL"
|
||||
value={formData.termsOfServiceUrl || ""}
|
||||
onChange={(value) => handleFieldChange('termsOfServiceUrl', value)}
|
||||
/>
|
||||
|
||||
<EditableField
|
||||
label="Privacy Policy URL"
|
||||
value={formData.privacyPolicyUrl || ""}
|
||||
onChange={(value) => handleFieldChange('privacyPolicyUrl', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Risk Tier</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedRiskTier('CRITICAL')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedRiskTier === 'CRITICAL'
|
||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Critical
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedRiskTier('SIGNIFICANT')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedRiskTier === 'SIGNIFICANT'
|
||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Significant
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedRiskTier('GENERAL')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
selectedRiskTier === 'GENERAL'
|
||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{selectedRiskTier === 'CRITICAL' &&
|
||||
"Handles sensitive data, critical for platform operation"}
|
||||
{selectedRiskTier === 'SIGNIFICANT' &&
|
||||
"No user data access, but important for platform management"}
|
||||
{selectedRiskTier === 'GENERAL' &&
|
||||
"General vendor with minimal risk"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{data.node?.statusPageUrl && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Status Page</Label>
|
||||
</div>
|
||||
<a
|
||||
href={data.node.statusPageUrl}
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View Status Page
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.node.termsOfServiceUrl && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Terms of Service</Label>
|
||||
</div>
|
||||
<a
|
||||
href={data.node.termsOfServiceUrl}
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View Terms of Service
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.node.privacyPolicyUrl && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Privacy Policy</Label>
|
||||
</div>
|
||||
<a
|
||||
href={data.node.privacyPolicyUrl}
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View Privacy Policy
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<180daebbc0ea7b69991d7bad2005ea13>>
|
||||
* @generated SignedSource<<2d865a3f5deae54eed7c54b08df7bf0a>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -28,6 +28,7 @@ export type VendorOverviewPageQuery$data = {
|
||||
readonly statusPageUrl?: string | null | undefined;
|
||||
readonly termsOfServiceUrl?: string | null | undefined;
|
||||
readonly updatedAt?: any;
|
||||
readonly version?: number;
|
||||
};
|
||||
};
|
||||
export type VendorOverviewPageQuery = {
|
||||
@@ -133,6 +134,13 @@ v13 = {
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v14 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -163,7 +171,8 @@ return {
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v13/*: any*/)
|
||||
(v13/*: any*/),
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"type": "Vendor",
|
||||
"abstractKey": null
|
||||
@@ -210,7 +219,8 @@ return {
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v13/*: any*/)
|
||||
(v13/*: any*/),
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"type": "Vendor",
|
||||
"abstractKey": null
|
||||
@@ -221,16 +231,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "d732d532dc450d86b3e790500c59e0ee",
|
||||
"cacheID": "9163462a149138110e8573e0b28350f9",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorOverviewPageQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query VendorOverviewPageQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n createdAt\n updatedAt\n }\n id\n }\n}\n"
|
||||
"text": "query VendorOverviewPageQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0b7427451e68a951e2eecbc3f5c59456";
|
||||
(node as any).hash = "56567f61e1016e70d6993ac132ca8382";
|
||||
|
||||
export default node;
|
||||
|
||||
192
apps/console/src/pages/__generated__/VendorOverviewPageUpdateVendorMutation.graphql.ts
generated
Normal file
192
apps/console/src/pages/__generated__/VendorOverviewPageUpdateVendorMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* @generated SignedSource<<0c1d84470c9beda41d05161fad1608f2>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
|
||||
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
|
||||
export type UpdateVendorInput = {
|
||||
description?: string | null | undefined;
|
||||
expectedVersion: number;
|
||||
id: string;
|
||||
name?: string | null | undefined;
|
||||
privacyPolicyUrl?: string | null | undefined;
|
||||
riskTier?: RiskTier | null | undefined;
|
||||
serviceCriticality?: ServiceCriticality | null | undefined;
|
||||
serviceStartAt?: any | null | undefined;
|
||||
serviceTerminationAt?: any | null | undefined;
|
||||
statusPageUrl?: string | null | undefined;
|
||||
termsOfServiceUrl?: string | null | undefined;
|
||||
};
|
||||
export type VendorOverviewPageUpdateVendorMutation$variables = {
|
||||
input: UpdateVendorInput;
|
||||
};
|
||||
export type VendorOverviewPageUpdateVendorMutation$data = {
|
||||
readonly updateVendor: {
|
||||
readonly description: string;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly privacyPolicyUrl: string | null | undefined;
|
||||
readonly riskTier: RiskTier;
|
||||
readonly serviceCriticality: ServiceCriticality;
|
||||
readonly serviceStartAt: any;
|
||||
readonly serviceTerminationAt: any | null | undefined;
|
||||
readonly statusPageUrl: string | null | undefined;
|
||||
readonly termsOfServiceUrl: string | null | undefined;
|
||||
readonly updatedAt: any;
|
||||
readonly version: number;
|
||||
};
|
||||
};
|
||||
export type VendorOverviewPageUpdateVendorMutation = {
|
||||
response: VendorOverviewPageUpdateVendorMutation$data;
|
||||
variables: VendorOverviewPageUpdateVendorMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "Vendor",
|
||||
"kind": "LinkedField",
|
||||
"name": "updateVendor",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "serviceStartAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "serviceTerminationAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "serviceCriticality",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "riskTier",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "statusPageUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "termsOfServiceUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "privacyPolicyUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "d0dd4f7b7219b20cc57a57e25b2b1197",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation VendorOverviewPageUpdateVendorMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n updatedAt\n version\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "be093542424cdd4a329ec389312d0f83";
|
||||
|
||||
export default node;
|
||||
Reference in New Issue
Block a user