Add update vendor logic

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-18 14:24:10 +01:00
parent ebdf14645d
commit 2d5514e45d
17 changed files with 1414 additions and 303 deletions

View File

@@ -12,12 +12,17 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -31,11 +36,7 @@
"react-router": "^7.1.5", "react-router": "^7.1.5",
"relay-runtime": "^18.2.0", "relay-runtime": "^18.2.0",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7"
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.7", "@babel/core": "^7.26.7",

View 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,
}

View 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>
)
}

View 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 }

View File

@@ -19,6 +19,7 @@ import {
BreadcrumbProvider, BreadcrumbProvider,
useBreadcrumb, useBreadcrumb,
} from "@/contexts/BreadcrumbContext"; } from "@/contexts/BreadcrumbContext";
import { Toaster } from "@/components/ui/toaster";
function BreadcrumbNavigation() { function BreadcrumbNavigation() {
const location = useLocation(); const location = useLocation();
@@ -90,6 +91,7 @@ export default function ConsoleLayout() {
</header> </header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0"> <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<Outlet /> <Outlet />
<Toaster />
</div> </div>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View File

@@ -2,19 +2,24 @@
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; 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 { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
useMutation,
} from "react-relay"; } 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 type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext"; import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { UpdateVendorInput } from "./__generated__/VendorOverviewPageUpdateVendorMutation.graphql";
const vendorOverviewPageQuery = graphql` const vendorOverviewPageQuery = graphql`
query VendorOverviewPageQuery($vendorId: ID!) { query VendorOverviewPageQuery($vendorId: ID!) {
@@ -32,11 +37,75 @@ const vendorOverviewPageQuery = graphql`
privacyPolicyUrl privacyPolicyUrl
createdAt createdAt
updatedAt 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({ function VendorOverviewPageContent({
queryRef, queryRef,
}: { }: {
@@ -44,8 +113,94 @@ function VendorOverviewPageContent({
}) { }) {
const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef); const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb(); const { setBreadcrumbSegment } = useBreadcrumb();
const [selectedCriticality, setSelectedCriticality] = useState(data.node.serviceCriticality); const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
const [selectedRiskTier, setSelectedRiskTier] = useState(data.node.riskTier); 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(() => { useEffect(() => {
if (data.node?.name) { if (data.node?.name) {
@@ -54,14 +209,20 @@ function VendorOverviewPageContent({
}, [data.node?.name, setBreadcrumbSegment]); }, [data.node?.name, setBreadcrumbSegment]);
return ( return (
<>
<div className="space-y-6 p-4 md:p-6 lg:p-8"> <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="mx-auto max-w-4xl space-y-6">
<div className="space-y-2"> <EditableField
<h1 className="text-xl font-semibold text-gray-900"> label="Name"
{data.node.name} value={formData.name}
</h1> onChange={(value) => handleFieldChange('name', value)}
<p className="text-gray-600">{data.node.description}</p> />
</div>
<EditableField
label="Description"
value={formData.description}
onChange={(value) => handleFieldChange('description', value)}
/>
<Card className="p-6"> <Card className="p-6">
<div className="space-y-4"> <div className="space-y-4">
@@ -73,27 +234,19 @@ function VendorOverviewPageContent({
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <EditableField
<div className="flex items-center gap-2"> label="Service Start At"
<HelpCircle className="h-4 w-4 text-gray-400" /> value={formData.serviceStartAt}
<Label className="text-sm">Service Start Date</Label> type="datetime-local"
</div> onChange={(value) => handleFieldChange('serviceStartAt', value)}
<p className="text-sm text-gray-600"> />
{new Date(data.node.serviceStartAt).toLocaleDateString()}
</p>
</div>
{data.node?.serviceTerminationAt && ( <EditableField
<div className="space-y-2"> label="Service Termination At"
<div className="flex items-center gap-2"> value={formData.serviceTerminationAt}
<HelpCircle className="h-4 w-4 text-gray-400" /> type="datetime-local"
<Label className="text-sm">Service Termination At</Label> onChange={(value) => handleFieldChange('serviceTerminationAt', value)}
</div> />
<p className="text-sm text-gray-600">
{new Date(data.node.serviceTerminationAt).toLocaleDateString()}
</p>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -102,10 +255,10 @@ function VendorOverviewPageContent({
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setSelectedCriticality('LOW')} onClick={() => handleFieldChange('serviceCriticality', 'LOW')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'LOW' formData.serviceCriticality === 'LOW'
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2" ? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -113,10 +266,10 @@ function VendorOverviewPageContent({
Low Low
</button> </button>
<button <button
onClick={() => setSelectedCriticality('MEDIUM')} onClick={() => handleFieldChange('serviceCriticality', 'MEDIUM')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'MEDIUM' formData.serviceCriticality === 'MEDIUM'
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2" ? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -124,10 +277,10 @@ function VendorOverviewPageContent({
Medium Medium
</button> </button>
<button <button
onClick={() => setSelectedCriticality('HIGH')} onClick={() => handleFieldChange('serviceCriticality', 'HIGH')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'HIGH' formData.serviceCriticality === 'HIGH'
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2" ? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -136,11 +289,11 @@ function VendorOverviewPageContent({
</button> </button>
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{selectedCriticality === 'HIGH' && {formData.serviceCriticality === 'HIGH' &&
"Critical service - downtime severely impacts end-users"} "Critical service - downtime severely impacts end-users"}
{selectedCriticality === 'MEDIUM' && {formData.serviceCriticality === 'MEDIUM' &&
"Important service - downtime moderately affects end-users"} "Important service - downtime moderately affects end-users"}
{selectedCriticality === 'LOW' && {formData.serviceCriticality === 'LOW' &&
"Non-critical service - minimal end-user impact if down"} "Non-critical service - minimal end-user impact if down"}
</p> </p>
</div> </div>
@@ -152,10 +305,10 @@ function VendorOverviewPageContent({
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setSelectedRiskTier('CRITICAL')} onClick={() => handleFieldChange('riskTier', 'CRITICAL')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'CRITICAL' formData.riskTier === 'CRITICAL'
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2" ? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -163,10 +316,10 @@ function VendorOverviewPageContent({
Critical Critical
</button> </button>
<button <button
onClick={() => setSelectedRiskTier('SIGNIFICANT')} onClick={() => handleFieldChange('riskTier', 'SIGNIFICANT')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'SIGNIFICANT' formData.riskTier === 'SIGNIFICANT'
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2" ? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -174,10 +327,10 @@ function VendorOverviewPageContent({
Significant Significant
</button> </button>
<button <button
onClick={() => setSelectedRiskTier('GENERAL')} onClick={() => handleFieldChange('riskTier', 'GENERAL')}
className={cn( className={cn(
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'GENERAL' formData.riskTier === 'GENERAL'
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2" ? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200" : "bg-gray-100 text-gray-900 hover:bg-gray-200"
)} )}
@@ -186,73 +339,55 @@ function VendorOverviewPageContent({
</button> </button>
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{selectedRiskTier === 'CRITICAL' && {formData.riskTier === 'CRITICAL' &&
"Handles sensitive data, critical for platform operation"} "Handles sensitive data, critical for platform operation"}
{selectedRiskTier === 'SIGNIFICANT' && {formData.riskTier === 'SIGNIFICANT' &&
"No user data access, but important for platform management"} "No user data access, but important for platform management"}
{selectedRiskTier === 'GENERAL' && {formData.riskTier === 'GENERAL' &&
"General vendor with minimal risk"} "General vendor with minimal risk"}
</p> </p>
</div> </div>
{data.node?.statusPageUrl && ( <EditableField
<div className="space-y-2"> label="Status Page URL"
<div className="flex items-center gap-2"> value={formData.statusPageUrl || ""}
<HelpCircle className="h-4 w-4 text-gray-400" /> onChange={(value) => handleFieldChange('statusPageUrl', value)}
<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 && ( <EditableField
<div className="space-y-2"> label="Terms of Service URL"
<div className="flex items-center gap-2"> value={formData.termsOfServiceUrl || ""}
<HelpCircle className="h-4 w-4 text-gray-400" /> onChange={(value) => handleFieldChange('termsOfServiceUrl', value)}
<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 && ( <EditableField
<div className="space-y-2"> label="Privacy Policy URL"
<div className="flex items-center gap-2"> value={formData.privacyPolicyUrl || ""}
<HelpCircle className="h-4 w-4 text-gray-400" /> onChange={(value) => handleFieldChange('privacyPolicyUrl', value)}
<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>
</div> </div>
</Card> </Card>
</div> </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>
)}
</>
); );
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<180daebbc0ea7b69991d7bad2005ea13>> * @generated SignedSource<<2d865a3f5deae54eed7c54b08df7bf0a>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -28,6 +28,7 @@ export type VendorOverviewPageQuery$data = {
readonly statusPageUrl?: string | null | undefined; readonly statusPageUrl?: string | null | undefined;
readonly termsOfServiceUrl?: string | null | undefined; readonly termsOfServiceUrl?: string | null | undefined;
readonly updatedAt?: any; readonly updatedAt?: any;
readonly version?: number;
}; };
}; };
export type VendorOverviewPageQuery = { export type VendorOverviewPageQuery = {
@@ -133,6 +134,13 @@ v13 = {
"kind": "ScalarField", "kind": "ScalarField",
"name": "updatedAt", "name": "updatedAt",
"storageKey": null "storageKey": null
},
v14 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
}; };
return { return {
"fragment": { "fragment": {
@@ -163,7 +171,8 @@ return {
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v12/*: any*/), (v12/*: any*/),
(v13/*: any*/) (v13/*: any*/),
(v14/*: any*/)
], ],
"type": "Vendor", "type": "Vendor",
"abstractKey": null "abstractKey": null
@@ -210,7 +219,8 @@ return {
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v12/*: any*/), (v12/*: any*/),
(v13/*: any*/) (v13/*: any*/),
(v14/*: any*/)
], ],
"type": "Vendor", "type": "Vendor",
"abstractKey": null "abstractKey": null
@@ -221,16 +231,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "d732d532dc450d86b3e790500c59e0ee", "cacheID": "9163462a149138110e8573e0b28350f9",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorOverviewPageQuery", "name": "VendorOverviewPageQuery",
"operationKind": "query", "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; export default node;

View 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;

35
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -2935,6 +2936,40 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",

View File

@@ -116,8 +116,6 @@ type Vendor implements Node {
id: ID! id: ID!
name: String! name: String!
description: String! description: String!
createdAt: Datetime!
updatedAt: Datetime!
serviceStartAt: Datetime! serviceStartAt: Datetime!
serviceTerminationAt: Datetime serviceTerminationAt: Datetime
serviceCriticality: ServiceCriticality! serviceCriticality: ServiceCriticality!
@@ -125,6 +123,9 @@ type Vendor implements Node {
statusPageUrl: String statusPageUrl: String
termsOfServiceUrl: String termsOfServiceUrl: String
privacyPolicyUrl: String privacyPolicyUrl: String
createdAt: Datetime!
updatedAt: Datetime!
version: Int!
} }
type FrameworkConnection { type FrameworkConnection {
@@ -313,6 +314,7 @@ type Query {
type Mutation { type Mutation {
createVendor(input: CreateVendorInput!): Vendor! createVendor(input: CreateVendorInput!): Vendor!
updateVendor(input: UpdateVendorInput!): Vendor!
deleteVendor(input: DeleteVendorInput!): Void! deleteVendor(input: DeleteVendorInput!): Void!
deletePeople(input: DeletePeopleInput!): Void! deletePeople(input: DeletePeopleInput!): Void!
createPeople(input: CreatePeopleInput!): People! createPeople(input: CreatePeopleInput!): People!
@@ -353,6 +355,7 @@ enum RiskTier @goModel(model: "github.com/getprobo/probo/pkg/probo/coredata.Risk
input UpdateVendorInput { input UpdateVendorInput {
id: ID! id: ID!
expectedVersion: Int!
name: String name: String
description: String description: String
serviceStartAt: Datetime serviceStartAt: Datetime

View File

@@ -160,6 +160,7 @@ type ComplexityRoot struct {
CreateVendor func(childComplexity int, input types.CreateVendorInput) int CreateVendor func(childComplexity int, input types.CreateVendorInput) int
DeletePeople func(childComplexity int, input types.DeletePeopleInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int
DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int
UpdateVendor func(childComplexity int, input types.UpdateVendorInput) int
} }
Organization struct { Organization struct {
@@ -257,6 +258,7 @@ type ComplexityRoot struct {
StatusPageURL func(childComplexity int) int StatusPageURL func(childComplexity int) int
TermsOfServiceURL func(childComplexity int) int TermsOfServiceURL func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
} }
VendorConnection struct { VendorConnection struct {
@@ -282,6 +284,7 @@ type FrameworkResolver interface {
} }
type MutationResolver interface { type MutationResolver interface {
CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.Vendor, error) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.Vendor, error)
UpdateVendor(ctx context.Context, input types.UpdateVendorInput) (*types.Vendor, error)
DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (string, error) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (string, error)
DeletePeople(ctx context.Context, input types.DeletePeopleInput) (string, error) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (string, error)
CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.People, error) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.People, error)
@@ -771,6 +774,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.DeleteVendor(childComplexity, args["input"].(types.DeleteVendorInput)), true return e.complexity.Mutation.DeleteVendor(childComplexity, args["input"].(types.DeleteVendorInput)), true
case "Mutation.updateVendor":
if e.complexity.Mutation.UpdateVendor == nil {
break
}
args, err := ec.field_Mutation_updateVendor_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateVendor(childComplexity, args["input"].(types.UpdateVendorInput)), true
case "Organization.createdAt": case "Organization.createdAt":
if e.complexity.Organization.CreatedAt == nil { if e.complexity.Organization.CreatedAt == nil {
break break
@@ -1207,6 +1222,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Vendor.UpdatedAt(childComplexity), true return e.complexity.Vendor.UpdatedAt(childComplexity), true
case "Vendor.version":
if e.complexity.Vendor.Version == nil {
break
}
return e.complexity.Vendor.Version(childComplexity), true
case "VendorConnection.edges": case "VendorConnection.edges":
if e.complexity.VendorConnection.Edges == nil { if e.complexity.VendorConnection.Edges == nil {
break break
@@ -1463,8 +1485,6 @@ type Vendor implements Node {
id: ID! id: ID!
name: String! name: String!
description: String! description: String!
createdAt: Datetime!
updatedAt: Datetime!
serviceStartAt: Datetime! serviceStartAt: Datetime!
serviceTerminationAt: Datetime serviceTerminationAt: Datetime
serviceCriticality: ServiceCriticality! serviceCriticality: ServiceCriticality!
@@ -1472,6 +1492,9 @@ type Vendor implements Node {
statusPageUrl: String statusPageUrl: String
termsOfServiceUrl: String termsOfServiceUrl: String
privacyPolicyUrl: String privacyPolicyUrl: String
createdAt: Datetime!
updatedAt: Datetime!
version: Int!
} }
type FrameworkConnection { type FrameworkConnection {
@@ -1660,6 +1683,7 @@ type Query {
type Mutation { type Mutation {
createVendor(input: CreateVendorInput!): Vendor! createVendor(input: CreateVendorInput!): Vendor!
updateVendor(input: UpdateVendorInput!): Vendor!
deleteVendor(input: DeleteVendorInput!): Void! deleteVendor(input: DeleteVendorInput!): Void!
deletePeople(input: DeletePeopleInput!): Void! deletePeople(input: DeletePeopleInput!): Void!
createPeople(input: CreatePeopleInput!): People! createPeople(input: CreatePeopleInput!): People!
@@ -1700,6 +1724,7 @@ enum RiskTier @goModel(model: "github.com/getprobo/probo/pkg/probo/coredata.Risk
input UpdateVendorInput { input UpdateVendorInput {
id: ID! id: ID!
expectedVersion: Int!
name: String name: String
description: String description: String
serviceStartAt: Datetime serviceStartAt: Datetime
@@ -1709,7 +1734,8 @@ input UpdateVendorInput {
statusPageUrl: String statusPageUrl: String
termsOfServiceUrl: String termsOfServiceUrl: String
privacyPolicyUrl: String privacyPolicyUrl: String
}`, BuiltIn: false}, }
`, BuiltIn: false},
} }
var parsedSchema = gqlparser.MustLoadSchema(sources...) var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -2117,6 +2143,29 @@ func (ec *executionContext) field_Mutation_deleteVendor_argsInput(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Mutation_updateVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_updateVendor_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateVendor_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.UpdateVendorInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNUpdateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateVendorInput(ctx, tmp)
}
var zeroVal types.UpdateVendorInput
return zeroVal, nil
}
func (ec *executionContext) field_Organization_frameworks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Organization_frameworks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -4923,10 +4972,6 @@ func (ec *executionContext) fieldContext_Mutation_createVendor(ctx context.Conte
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "description": case "description":
return ec.fieldContext_Vendor_description(ctx, field) return ec.fieldContext_Vendor_description(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
case "serviceStartAt": case "serviceStartAt":
return ec.fieldContext_Vendor_serviceStartAt(ctx, field) return ec.fieldContext_Vendor_serviceStartAt(ctx, field)
case "serviceTerminationAt": case "serviceTerminationAt":
@@ -4941,6 +4986,12 @@ func (ec *executionContext) fieldContext_Mutation_createVendor(ctx context.Conte
return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field) return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field)
case "privacyPolicyUrl": case "privacyPolicyUrl":
return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field) return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
case "version":
return ec.fieldContext_Vendor_version(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name) return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name)
}, },
@@ -4953,6 +5004,77 @@ func (ec *executionContext) fieldContext_Mutation_createVendor(ctx context.Conte
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_updateVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_updateVendor(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UpdateVendor(rctx, fc.Args["input"].(types.UpdateVendorInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.Vendor)
fc.Result = res
return ec.marshalNVendor2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_updateVendor(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Vendor_id(ctx, field)
case "name":
return ec.fieldContext_Vendor_name(ctx, field)
case "description":
return ec.fieldContext_Vendor_description(ctx, field)
case "serviceStartAt":
return ec.fieldContext_Vendor_serviceStartAt(ctx, field)
case "serviceTerminationAt":
return ec.fieldContext_Vendor_serviceTerminationAt(ctx, field)
case "serviceCriticality":
return ec.fieldContext_Vendor_serviceCriticality(ctx, field)
case "riskTier":
return ec.fieldContext_Vendor_riskTier(ctx, field)
case "statusPageUrl":
return ec.fieldContext_Vendor_statusPageUrl(ctx, field)
case "termsOfServiceUrl":
return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field)
case "privacyPolicyUrl":
return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
case "version":
return ec.fieldContext_Vendor_version(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name)
},
}
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updateVendor_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_deleteVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_deleteVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_deleteVendor(ctx, field) fc, err := ec.fieldContext_Mutation_deleteVendor(ctx, field)
if err != nil { if err != nil {
@@ -7215,82 +7337,6 @@ func (ec *executionContext) fieldContext_Vendor_description(_ context.Context, f
return fc, nil return fc, nil
} }
func (ec *executionContext) _Vendor_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_createdAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.CreatedAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Vendor_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Vendor_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_updatedAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.UpdatedAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Vendor_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Vendor_serviceStartAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) { func (ec *executionContext) _Vendor_serviceStartAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_serviceStartAt(ctx, field) fc, err := ec.fieldContext_Vendor_serviceStartAt(ctx, field)
if err != nil { if err != nil {
@@ -7545,6 +7591,120 @@ func (ec *executionContext) fieldContext_Vendor_privacyPolicyUrl(_ context.Conte
return fc, nil return fc, nil
} }
func (ec *executionContext) _Vendor_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_createdAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.CreatedAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Vendor_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Vendor_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_updatedAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.UpdatedAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Vendor_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Vendor_version(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Vendor_version(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Version, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Vendor_version(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Vendor",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _VendorConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.VendorConnection) (ret graphql.Marshaler) { func (ec *executionContext) _VendorConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.VendorConnection) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_VendorConnection_edges(ctx, field) fc, err := ec.fieldContext_VendorConnection_edges(ctx, field)
if err != nil { if err != nil {
@@ -7714,10 +7874,6 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel
return ec.fieldContext_Vendor_name(ctx, field) return ec.fieldContext_Vendor_name(ctx, field)
case "description": case "description":
return ec.fieldContext_Vendor_description(ctx, field) return ec.fieldContext_Vendor_description(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
case "serviceStartAt": case "serviceStartAt":
return ec.fieldContext_Vendor_serviceStartAt(ctx, field) return ec.fieldContext_Vendor_serviceStartAt(ctx, field)
case "serviceTerminationAt": case "serviceTerminationAt":
@@ -7732,6 +7888,12 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel
return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field) return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field)
case "privacyPolicyUrl": case "privacyPolicyUrl":
return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field) return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
case "version":
return ec.fieldContext_Vendor_version(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name) return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name)
}, },
@@ -9440,7 +9602,7 @@ func (ec *executionContext) unmarshalInputUpdateVendorInput(ctx context.Context,
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"id", "name", "description", "serviceStartAt", "serviceTerminationAt", "serviceCriticality", "riskTier", "statusPageUrl", "termsOfServiceUrl", "privacyPolicyUrl"} fieldsInOrder := [...]string{"id", "expectedVersion", "name", "description", "serviceStartAt", "serviceTerminationAt", "serviceCriticality", "riskTier", "statusPageUrl", "termsOfServiceUrl", "privacyPolicyUrl"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -9454,6 +9616,13 @@ func (ec *executionContext) unmarshalInputUpdateVendorInput(ctx context.Context,
return it, err return it, err
} }
it.ID = data it.ID = data
case "expectedVersion":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("expectedVersion"))
data, err := ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
it.ExpectedVersion = data
case "name": case "name":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v) data, err := ec.unmarshalOString2ᚖstring(ctx, v)
@@ -10492,6 +10661,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "updateVendor":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updateVendor(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "deleteVendor": case "deleteVendor":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_deleteVendor(ctx, field) return ec._Mutation_deleteVendor(ctx, field)
@@ -11346,16 +11522,6 @@ func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, o
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "createdAt":
out.Values[i] = ec._Vendor_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "updatedAt":
out.Values[i] = ec._Vendor_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "serviceStartAt": case "serviceStartAt":
out.Values[i] = ec._Vendor_serviceStartAt(ctx, field, obj) out.Values[i] = ec._Vendor_serviceStartAt(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -11379,6 +11545,21 @@ func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, o
out.Values[i] = ec._Vendor_termsOfServiceUrl(ctx, field, obj) out.Values[i] = ec._Vendor_termsOfServiceUrl(ctx, field, obj)
case "privacyPolicyUrl": case "privacyPolicyUrl":
out.Values[i] = ec._Vendor_privacyPolicyUrl(ctx, field, obj) out.Values[i] = ec._Vendor_privacyPolicyUrl(ctx, field, obj)
case "createdAt":
out.Values[i] = ec._Vendor_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "updatedAt":
out.Values[i] = ec._Vendor_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "version":
out.Values[i] = ec._Vendor_version(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -12730,6 +12911,11 @@ func (ec *executionContext) marshalNTaskStateTransitionEdge2ᚖgithubᚗcomᚋge
return ec._TaskStateTransitionEdge(ctx, sel, v) return ec._TaskStateTransitionEdge(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNUpdateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateVendorInput(ctx context.Context, v any) (types.UpdateVendorInput, error) {
res, err := ec.unmarshalInputUpdateVendorInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNVendor2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v types.Vendor) graphql.Marshaler { func (ec *executionContext) marshalNVendor2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v types.Vendor) graphql.Marshaler {
return ec._Vendor(ctx, sel, &v) return ec._Vendor(ctx, sel, &v)
} }

View File

@@ -240,6 +240,7 @@ type TaskStateTransitionEdge struct {
type UpdateVendorInput struct { type UpdateVendorInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ServiceStartAt *time.Time `json:"serviceStartAt,omitempty"` ServiceStartAt *time.Time `json:"serviceStartAt,omitempty"`
@@ -255,8 +256,6 @@ type Vendor struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ServiceStartAt time.Time `json:"serviceStartAt"` ServiceStartAt time.Time `json:"serviceStartAt"`
ServiceTerminationAt *time.Time `json:"serviceTerminationAt,omitempty"` ServiceTerminationAt *time.Time `json:"serviceTerminationAt,omitempty"`
ServiceCriticality coredata.ServiceCriticality `json:"serviceCriticality"` ServiceCriticality coredata.ServiceCriticality `json:"serviceCriticality"`
@@ -264,6 +263,9 @@ type Vendor struct {
StatusPageURL *string `json:"statusPageUrl,omitempty"` StatusPageURL *string `json:"statusPageUrl,omitempty"`
TermsOfServiceURL *string `json:"termsOfServiceUrl,omitempty"` TermsOfServiceURL *string `json:"termsOfServiceUrl,omitempty"`
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"` PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Version int `json:"version"`
} }
func (Vendor) IsNode() {} func (Vendor) IsNode() {}

View File

@@ -53,5 +53,6 @@ func NewVendor(v *coredata.Vendor) *Vendor {
StatusPageURL: v.StatusPageURL, StatusPageURL: v.StatusPageURL,
TermsOfServiceURL: v.TermsOfServiceURL, TermsOfServiceURL: v.TermsOfServiceURL,
PrivacyPolicyURL: v.PrivacyPolicyURL, PrivacyPolicyURL: v.PrivacyPolicyURL,
Version: v.Version,
} }
} }

View File

@@ -78,6 +78,28 @@ func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateV
return types.NewVendor(vendor), nil return types.NewVendor(vendor), nil
} }
// UpdateVendor is the resolver for the updateVendor field.
func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateVendorInput) (*types.Vendor, error) {
vendor, err := r.svc.UpdateVendor(ctx, probo.UpdateVendorRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Description: input.Description,
ServiceStartAt: input.ServiceStartAt,
ServiceTerminationAt: input.ServiceTerminationAt,
ServiceCriticality: input.ServiceCriticality,
RiskTier: input.RiskTier,
StatusPageURL: input.StatusPageURL,
TermsOfServiceURL: input.TermsOfServiceURL,
PrivacyPolicyURL: input.PrivacyPolicyURL,
})
if err != nil {
return nil, fmt.Errorf("cannot update vendor: %w", err)
}
return types.NewVendor(vendor), nil
}
// DeleteVendor is the resolver for the deleteVendor field. // DeleteVendor is the resolver for the deleteVendor field.
func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (string, error) { func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (string, error) {
err := r.svc.DeleteVendor(ctx, input.VendorID) err := r.svc.DeleteVendor(ctx, input.VendorID)

View File

@@ -0,0 +1 @@
ALTER TABLE vendors ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

View File

@@ -16,6 +16,7 @@ package coredata
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"maps" "maps"
"time" "time"
@@ -26,6 +27,8 @@ import (
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
var ErrConcurrentModification = errors.New("concurrent modification")
type ( type (
Vendor struct { Vendor struct {
ID gid.GID ID gid.GID
@@ -41,9 +44,23 @@ type (
PrivacyPolicyURL *string PrivacyPolicyURL *string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Version int
} }
Vendors []*Vendor Vendors []*Vendor
UpdateVendorParams struct {
ExpectedVersion int
Name *string
Description *string
ServiceStartAt *time.Time
ServiceTerminationAt *time.Time
ServiceCriticality *ServiceCriticality
RiskTier *RiskTier
StatusPageURL *string
TermsOfServiceURL *string
PrivacyPolicyURL *string
}
) )
func (v Vendor) CursorKey() page.CursorKey { func (v Vendor) CursorKey() page.CursorKey {
@@ -65,6 +82,7 @@ func (v *Vendor) scan(r pgx.Row) error {
&v.PrivacyPolicyURL, &v.PrivacyPolicyURL,
&v.CreatedAt, &v.CreatedAt,
&v.UpdatedAt, &v.UpdatedAt,
&v.Version,
) )
} }
@@ -88,7 +106,8 @@ SELECT
terms_of_service_url, terms_of_service_url,
privacy_policy_url, privacy_policy_url,
created_at, created_at,
updated_at updated_at,
version
FROM FROM
vendors vendors
WHERE WHERE
@@ -133,7 +152,8 @@ INSERT INTO
terms_of_service_url, terms_of_service_url,
privacy_policy_url, privacy_policy_url,
created_at, created_at,
updated_at updated_at,
version
) )
VALUES ( VALUES (
@vendor_id, @vendor_id,
@@ -148,7 +168,8 @@ VALUES (
@terms_of_service_url, @terms_of_service_url,
@privacy_policy_url, @privacy_policy_url,
@created_at, @created_at,
@updated_at @updated_at,
1
) )
` `
@@ -210,7 +231,8 @@ SELECT
terms_of_service_url, terms_of_service_url,
privacy_policy_url, privacy_policy_url,
created_at, created_at,
updated_at updated_at,
version
FROM FROM
vendors vendors
WHERE WHERE
@@ -249,3 +271,93 @@ WHERE
return nil return nil
} }
func (v *Vendor) Update(
ctx context.Context,
conn pg.Conn,
scope *Scope,
params UpdateVendorParams,
) error {
q := `
UPDATE vendors SET
name = COALESCE(@name, name),
description = COALESCE(@description, description),
service_start_at = COALESCE(@service_start_at, service_start_at),
service_termination_at = COALESCE(@service_termination_at, service_termination_at),
service_criticality = COALESCE(@service_criticality, service_criticality),
risk_tier = COALESCE(@risk_tier, risk_tier),
status_page_url = COALESCE(@status_page_url, status_page_url),
terms_of_service_url = COALESCE(@terms_of_service_url, terms_of_service_url),
privacy_policy_url = COALESCE(@privacy_policy_url, privacy_policy_url),
updated_at = @updated_at,
version = version + 1
WHERE %s
AND id = @vendor_id
AND version = @expected_version
RETURNING
id,
organization_id,
name,
description,
service_start_at,
service_termination_at,
service_criticality,
risk_tier,
status_page_url,
terms_of_service_url,
privacy_policy_url,
created_at,
updated_at,
version
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"vendor_id": v.ID,
"expected_version": params.ExpectedVersion,
"updated_at": time.Now(),
}
if params.Name != nil {
args["name"] = *params.Name
}
if params.Description != nil {
args["description"] = *params.Description
}
if params.ServiceStartAt != nil {
args["service_start_at"] = *params.ServiceStartAt
}
if params.ServiceTerminationAt != nil {
args["service_termination_at"] = *params.ServiceTerminationAt
}
if params.ServiceCriticality != nil {
args["service_criticality"] = *params.ServiceCriticality
}
if params.RiskTier != nil {
args["risk_tier"] = *params.RiskTier
}
if params.StatusPageURL != nil {
args["status_page_url"] = *params.StatusPageURL
}
if params.TermsOfServiceURL != nil {
args["terms_of_service_url"] = *params.TermsOfServiceURL
}
if params.PrivacyPolicyURL != nil {
args["privacy_policy_url"] = *params.PrivacyPolicyURL
}
maps.Copy(args, scope.SQLArguments())
r := conn.QueryRow(ctx, q, args)
v2 := Vendor{}
if err := v2.scan(r); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrConcurrentModification
}
return err
}
*v = v2
return nil
}

View File

@@ -0,0 +1,55 @@
package probo
import (
"context"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/probo/coredata"
"go.gearno.de/kit/pg"
)
type UpdateVendorRequest struct {
ID gid.GID
ExpectedVersion int
Name *string
Description *string
ServiceStartAt *time.Time
ServiceTerminationAt *time.Time
ServiceCriticality *coredata.ServiceCriticality
RiskTier *coredata.RiskTier
StatusPageURL *string
TermsOfServiceURL *string
PrivacyPolicyURL *string
}
func (s Service) UpdateVendor(
ctx context.Context,
req UpdateVendorRequest,
) (*coredata.Vendor, error) {
params := coredata.UpdateVendorParams{
ExpectedVersion: req.ExpectedVersion,
Name: req.Name,
Description: req.Description,
ServiceStartAt: req.ServiceStartAt,
ServiceTerminationAt: req.ServiceTerminationAt,
ServiceCriticality: req.ServiceCriticality,
RiskTier: req.RiskTier,
StatusPageURL: req.StatusPageURL,
TermsOfServiceURL: req.TermsOfServiceURL,
PrivacyPolicyURL: req.PrivacyPolicyURL,
}
vendor := &coredata.Vendor{ID: req.ID}
err := s.pg.WithTx(
ctx,
func(conn pg.Conn) error {
return vendor.Update(ctx, conn, s.scope, params)
})
if err != nil {
return nil, err
}
return vendor, nil
}