Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
This commit is contained in:
+76
-16
@@ -2,52 +2,112 @@
|
||||
* Secure authentication token storage
|
||||
*
|
||||
* This module provides an abstraction for storing authentication tokens
|
||||
* securely. In production, tokens should be stored in httpOnly cookies,
|
||||
* but for development we can use a more permissive approach.
|
||||
* securely using httpOnly cookies via API endpoints.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Store authentication token
|
||||
* Store authentication token via httpOnly cookie
|
||||
*/
|
||||
export function setAuthToken(token: string): void {
|
||||
export async function setAuthToken(token: string): Promise<void> {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
// In production, this should set an httpOnly cookie via API
|
||||
// For now, we'll use sessionStorage as a compromise (better than localStorage)
|
||||
// TODO: Implement httpOnly cookie storage via API endpoint
|
||||
sessionStorage.setItem('auth_token', token)
|
||||
try {
|
||||
// Store in httpOnly cookie via API endpoint
|
||||
const response = await fetch('/api/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set token')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set auth token:', error)
|
||||
// Fallback to sessionStorage if API fails
|
||||
sessionStorage.setItem('auth_token', token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication token
|
||||
* Get authentication token from httpOnly cookie
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
export async function getAuthToken(): Promise<string | null> {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Get from httpOnly cookie via API endpoint
|
||||
try {
|
||||
// Try to get from httpOnly cookie via API
|
||||
const response = await fetch('/api/auth/token')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return data.token || null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get auth token:', error)
|
||||
}
|
||||
|
||||
// Fallback to sessionStorage
|
||||
return sessionStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove authentication token
|
||||
* Synchronous get auth token (for immediate use)
|
||||
* Falls back to sessionStorage if cookie not available
|
||||
*/
|
||||
export function removeAuthToken(): void {
|
||||
export function getAuthTokenSync(): string | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check sessionStorage first (faster for immediate use)
|
||||
return sessionStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication token
|
||||
*/
|
||||
export async function clearAuthToken(): Promise<void> {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Clear httpOnly cookie via API endpoint
|
||||
await fetch('/api/auth/token', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to clear auth token:', error)
|
||||
}
|
||||
|
||||
// Also clear sessionStorage as fallback
|
||||
sessionStorage.removeItem('auth_token')
|
||||
// TODO: Clear httpOnly cookie via API endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove authentication token (alias for clearAuthToken)
|
||||
*/
|
||||
export async function removeAuthToken(): Promise<void> {
|
||||
return clearAuthToken()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
return getAuthToken() !== null
|
||||
export async function isAuthenticated(): Promise<boolean> {
|
||||
const token = await getAuthToken()
|
||||
return token !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous check if user is authenticated
|
||||
*/
|
||||
export function isAuthenticatedSync(): boolean {
|
||||
return getAuthTokenSync() !== null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* SSO (Single Sign-On) integration for Phoenix Nexus
|
||||
*
|
||||
* This module handles SSO flow between the public site and portals
|
||||
* using Keycloak as the identity provider.
|
||||
*/
|
||||
|
||||
export interface SSOConfig {
|
||||
keycloakUrl: string
|
||||
realm: string
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
}
|
||||
|
||||
export interface UserRole {
|
||||
role: 'admin' | 'developer' | 'partner' | 'tenant-admin' | 'user'
|
||||
portal: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which portal to redirect to based on user role
|
||||
*/
|
||||
export function getPortalForRole(role: string): string {
|
||||
const roleMap: Record<string, string> = {
|
||||
admin: '/portal',
|
||||
'tenant-admin': '/portal/admin',
|
||||
developer: '/portal/developers',
|
||||
partner: '/portal/partners',
|
||||
user: '/portal',
|
||||
}
|
||||
|
||||
return roleMap[role] || '/portal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate SSO flow
|
||||
*/
|
||||
export async function initiateSSO(config: SSOConfig): Promise<string> {
|
||||
const authUrl = new URL(
|
||||
`${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/auth`
|
||||
)
|
||||
|
||||
authUrl.searchParams.set('client_id', config.clientId)
|
||||
authUrl.searchParams.set('redirect_uri', config.redirectUri)
|
||||
authUrl.searchParams.set('response_type', 'code')
|
||||
authUrl.searchParams.set('scope', 'openid profile email')
|
||||
authUrl.searchParams.set('state', generateState())
|
||||
|
||||
return authUrl.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SSO callback
|
||||
*/
|
||||
export async function handleSSOCallback(
|
||||
code: string,
|
||||
state: string,
|
||||
config: SSOConfig
|
||||
): Promise<{ token: string; user: any; role: string }> {
|
||||
// Exchange code for token
|
||||
const tokenUrl = `${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/token`
|
||||
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: config.redirectUri,
|
||||
client_id: config.clientId,
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET || '',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to exchange code for token')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Get user info
|
||||
const userInfoUrl = `${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/userinfo`
|
||||
const userResponse = await fetch(userInfoUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
const user = await userResponse.json()
|
||||
|
||||
// Extract role from token or user info
|
||||
const role = extractRoleFromToken(data.access_token) || user.role || 'user'
|
||||
|
||||
return {
|
||||
token: data.access_token,
|
||||
user,
|
||||
role,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate state parameter for OAuth flow
|
||||
*/
|
||||
function generateState(): string {
|
||||
return Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract role from JWT token
|
||||
*/
|
||||
function extractRoleFromToken(token: string): string | null {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
return payload.realm_access?.roles?.[0] || payload.role || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
// Check for token in localStorage or cookies
|
||||
if (typeof window === 'undefined') return false
|
||||
|
||||
return !!localStorage.getItem('phoenix_token') ||
|
||||
!!document.cookie.includes('phoenix_session')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out user
|
||||
*/
|
||||
export function signOut(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
localStorage.removeItem('phoenix_token')
|
||||
document.cookie = 'phoenix_session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
|
||||
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export async function loadBrandContent(): Promise<BrandContent> {
|
||||
// In production, this would fetch from API, CMS, or database
|
||||
return {
|
||||
tagline: 'The sovereign cloud born of fire and ancestral wisdom.',
|
||||
mission: 'Phoenix Sankofa Cloud exists to build the world\'s first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return.',
|
||||
mission: 'Sankofa Phoenix exists to build the world\'s first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return. Sankofa Phoenix is part of the Sankofa ecosystem.',
|
||||
values: [
|
||||
'Remember: Where we came from',
|
||||
'Retrieve: What was essential',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Phoenix Sankofa Cloud Design System
|
||||
* Sankofa Phoenix Design System
|
||||
*
|
||||
* Design tokens and utilities for the brand
|
||||
*/
|
||||
|
||||
+55
-44
@@ -1,58 +1,69 @@
|
||||
import { ApolloClient, InMemoryCache, createHttpLink, from, ApolloLink } from '@apollo/client'
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { onError } from '@apollo/client/link/error'
|
||||
import { handleApiError, getUserFriendlyMessage } from '@/lib/error-handler'
|
||||
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
||||
import { createClient } from 'graphql-ws'
|
||||
|
||||
// HTTP Link - configured to use the GraphQL API
|
||||
const httpLink = createHttpLink({
|
||||
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
|
||||
const httpLink = new HttpLink({
|
||||
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:4000/graphql',
|
||||
})
|
||||
|
||||
// Auth Link - for adding authentication headers
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
// Get the authentication token from secure storage
|
||||
let token: string | null = null
|
||||
if (typeof window !== 'undefined') {
|
||||
// Try to get from secure storage
|
||||
token = sessionStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Error Link - handle GraphQL and network errors
|
||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
const error = handleApiError(new Error(message))
|
||||
console.error(
|
||||
`[GraphQL error]: Message: ${getUserFriendlyMessage(error)}, Location: ${locations}, Path: ${path}`
|
||||
// WebSocket link for subscriptions (only in browser)
|
||||
const wsLink =
|
||||
typeof window !== 'undefined'
|
||||
? new GraphQLWsLink(
|
||||
createClient({
|
||||
url: process.env.NEXT_PUBLIC_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql',
|
||||
connectionParams: () => {
|
||||
// Add auth token if available
|
||||
const token = localStorage.getItem('authToken')
|
||||
return token ? { authorization: `Bearer ${token}` } : {}
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
: null
|
||||
|
||||
if (networkError) {
|
||||
const error = handleApiError(networkError)
|
||||
console.error(`[Network error]: ${getUserFriendlyMessage(error)}`)
|
||||
}
|
||||
})
|
||||
// Split link: use WebSocket for subscriptions, HTTP for queries/mutations
|
||||
const splitLink =
|
||||
typeof window !== 'undefined' && wsLink
|
||||
? split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query)
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
)
|
||||
},
|
||||
wsLink,
|
||||
httpLink
|
||||
)
|
||||
: httpLink
|
||||
|
||||
// Create Apollo Client
|
||||
export const apolloClient = new ApolloClient({
|
||||
link: from([errorLink, authLink, httpLink]),
|
||||
cache: new InMemoryCache(),
|
||||
link: splitLink,
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
NetworkTopology: {
|
||||
fields: {
|
||||
nodes: {
|
||||
merge(existing = [], incoming) {
|
||||
return incoming
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
merge(existing = [], incoming) {
|
||||
return incoming
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
query: {
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './useAuth'
|
||||
export * from './useResources'
|
||||
export * from './useSites'
|
||||
export * from './useInfrastructure'
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import {
|
||||
GET_COUNTRIES,
|
||||
GET_COUNTRY,
|
||||
GET_NETWORK_TOPOLOGIES,
|
||||
GET_NETWORK_TOPOLOGY,
|
||||
GET_COMPLIANCE_REQUIREMENTS,
|
||||
GET_COMPLIANCE_REQUIREMENT,
|
||||
GET_DEPLOYMENT_MILESTONES,
|
||||
GET_DEPLOYMENT_MILESTONE,
|
||||
GET_COST_ESTIMATES,
|
||||
GET_COST_ESTIMATE,
|
||||
GET_INFRASTRUCTURE_SUMMARY,
|
||||
UPDATE_NETWORK_TOPOLOGY,
|
||||
CREATE_DEPLOYMENT_MILESTONE,
|
||||
UPDATE_DEPLOYMENT_MILESTONE,
|
||||
DELETE_DEPLOYMENT_MILESTONE,
|
||||
UPDATE_COMPLIANCE_REQUIREMENT,
|
||||
UPDATE_COST_ESTIMATE,
|
||||
} from '../queries/infrastructure'
|
||||
|
||||
export function useCountries(filter?: {
|
||||
region?: string
|
||||
priority?: string
|
||||
relationshipType?: string
|
||||
}) {
|
||||
return useQuery(GET_COUNTRIES, {
|
||||
variables: { filter },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useCountry(name: string) {
|
||||
return useQuery(GET_COUNTRY, {
|
||||
variables: { name },
|
||||
skip: !name,
|
||||
})
|
||||
}
|
||||
|
||||
export function useNetworkTopologies(filter?: { region?: string; entity?: string }) {
|
||||
return useQuery(GET_NETWORK_TOPOLOGIES, {
|
||||
variables: { filter },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useNetworkTopology(id: string) {
|
||||
return useQuery(GET_NETWORK_TOPOLOGY, {
|
||||
variables: { id },
|
||||
skip: !id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useComplianceRequirements(filter?: {
|
||||
country?: string
|
||||
region?: string
|
||||
status?: string
|
||||
framework?: string
|
||||
}) {
|
||||
return useQuery(GET_COMPLIANCE_REQUIREMENTS, {
|
||||
variables: { filter },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useComplianceRequirement(country: string) {
|
||||
return useQuery(GET_COMPLIANCE_REQUIREMENT, {
|
||||
variables: { country },
|
||||
skip: !country,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeploymentMilestones(filter?: {
|
||||
region?: string
|
||||
entity?: string
|
||||
priority?: string
|
||||
status?: string
|
||||
}) {
|
||||
return useQuery(GET_DEPLOYMENT_MILESTONES, {
|
||||
variables: { filter },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeploymentMilestone(id: string) {
|
||||
return useQuery(GET_DEPLOYMENT_MILESTONE, {
|
||||
variables: { id },
|
||||
skip: !id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCostEstimates(filter?: {
|
||||
region?: string
|
||||
entity?: string
|
||||
category?: string
|
||||
}) {
|
||||
return useQuery(GET_COST_ESTIMATES, {
|
||||
variables: { filter },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useCostEstimate(region: string, entity: string, category: string) {
|
||||
return useQuery(GET_COST_ESTIMATE, {
|
||||
variables: { region, entity, category },
|
||||
skip: !region || !entity || !category,
|
||||
})
|
||||
}
|
||||
|
||||
export function useInfrastructureSummary() {
|
||||
return useQuery(GET_INFRASTRUCTURE_SUMMARY, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateNetworkTopology() {
|
||||
const [mutate, { loading, error }] = useMutation(UPDATE_NETWORK_TOPOLOGY, {
|
||||
refetchQueries: [{ query: GET_NETWORK_TOPOLOGIES }],
|
||||
})
|
||||
|
||||
return {
|
||||
updateTopology: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function useCreateDeploymentMilestone() {
|
||||
const [mutate, { loading, error }] = useMutation(CREATE_DEPLOYMENT_MILESTONE, {
|
||||
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
|
||||
})
|
||||
|
||||
return {
|
||||
createMilestone: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateDeploymentMilestone() {
|
||||
const [mutate, { loading, error }] = useMutation(UPDATE_DEPLOYMENT_MILESTONE, {
|
||||
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
|
||||
})
|
||||
|
||||
return {
|
||||
updateMilestone: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function useDeleteDeploymentMilestone() {
|
||||
const [mutate, { loading, error }] = useMutation(DELETE_DEPLOYMENT_MILESTONE, {
|
||||
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
|
||||
})
|
||||
|
||||
return {
|
||||
deleteMilestone: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateComplianceRequirement() {
|
||||
const [mutate, { loading, error }] = useMutation(UPDATE_COMPLIANCE_REQUIREMENT, {
|
||||
refetchQueries: [{ query: GET_COMPLIANCE_REQUIREMENTS }],
|
||||
})
|
||||
|
||||
return {
|
||||
updateCompliance: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateCostEstimate() {
|
||||
const [mutate, { loading, error }] = useMutation(UPDATE_COST_ESTIMATE, {
|
||||
refetchQueries: [{ query: GET_COST_ESTIMATES }],
|
||||
})
|
||||
|
||||
return {
|
||||
updateCostEstimate: mutate,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
|
||||
import { useSubscription } from '@apollo/client'
|
||||
import { gql } from 'graphql-tag'
|
||||
import { useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
const RESOURCE_CREATED_SUBSCRIPTION = gql`
|
||||
subscription ResourceCreated {
|
||||
resourceCreated {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
site {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const RESOURCE_UPDATED_SUBSCRIPTION = gql`
|
||||
subscription ResourceUpdated($id: ID!) {
|
||||
resourceUpdated(id: $id) {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
metadata
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const RESOURCE_DELETED_SUBSCRIPTION = gql`
|
||||
subscription ResourceDeleted($id: ID!) {
|
||||
resourceDeleted(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export function useResourceCreated() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data, error } = useSubscription(RESOURCE_CREATED_SUBSCRIPTION)
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.resourceCreated) {
|
||||
// Invalidate queries to refetch updated data
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] })
|
||||
|
||||
// Optionally show toast notification
|
||||
if (typeof window !== 'undefined' && (window as any).toast) {
|
||||
;(window as any).toast({
|
||||
title: 'New Resource Created',
|
||||
description: `${data.resourceCreated.name} has been created`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [data, queryClient])
|
||||
|
||||
return { data: data?.resourceCreated, error }
|
||||
}
|
||||
|
||||
export function useResourceUpdated(resourceId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data, error } = useSubscription(RESOURCE_UPDATED_SUBSCRIPTION, {
|
||||
variables: { id: resourceId },
|
||||
skip: !resourceId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.resourceUpdated) {
|
||||
// Invalidate specific resource and list queries
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['resource', resourceId] })
|
||||
}
|
||||
}, [data, queryClient, resourceId])
|
||||
|
||||
return { data: data?.resourceUpdated, error }
|
||||
}
|
||||
|
||||
export function useResourceDeleted(resourceId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data, error } = useSubscription(RESOURCE_DELETED_SUBSCRIPTION, {
|
||||
variables: { id: resourceId },
|
||||
skip: !resourceId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.resourceDeleted) {
|
||||
// Remove from cache and invalidate queries
|
||||
queryClient.removeQueries({ queryKey: ['resource', resourceId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] })
|
||||
|
||||
if (typeof window !== 'undefined' && (window as any).toast) {
|
||||
;(window as any).toast({
|
||||
title: 'Resource Deleted',
|
||||
description: 'Resource has been deleted',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [data, queryClient, resourceId])
|
||||
|
||||
return { deletedId: data?.resourceDeleted, error }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useSubscription } from '@apollo/client'
|
||||
import {
|
||||
RESOURCE_UPDATED,
|
||||
METRICS_UPDATED,
|
||||
HEALTH_CHANGED,
|
||||
FINDING_CREATED,
|
||||
RISK_CREATED,
|
||||
} from '../subscriptions'
|
||||
|
||||
/**
|
||||
* Hook for subscribing to resource updates
|
||||
*/
|
||||
export function useResourceUpdate(resourceId: string) {
|
||||
const { data, loading, error } = useSubscription(RESOURCE_UPDATED, {
|
||||
variables: { resourceId },
|
||||
skip: !resourceId,
|
||||
})
|
||||
|
||||
return {
|
||||
resource: data?.resourceUpdated,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for subscribing to metrics updates
|
||||
*/
|
||||
export function useMetricsUpdate(resourceId: string, metricType: string) {
|
||||
const { data, loading, error } = useSubscription(METRICS_UPDATED, {
|
||||
variables: { resourceId, metricType },
|
||||
skip: !resourceId || !metricType,
|
||||
})
|
||||
|
||||
return {
|
||||
metric: data?.metricsUpdated,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for subscribing to health changes
|
||||
*/
|
||||
export function useHealthChange(resourceId: string) {
|
||||
const { data, loading, error } = useSubscription(HEALTH_CHANGED, {
|
||||
variables: { resourceId },
|
||||
skip: !resourceId,
|
||||
})
|
||||
|
||||
return {
|
||||
health: data?.healthChanged,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for subscribing to new findings
|
||||
*/
|
||||
export function useFindingCreated(controlId?: string) {
|
||||
const { data, loading, error } = useSubscription(FINDING_CREATED, {
|
||||
variables: { controlId: controlId || null },
|
||||
})
|
||||
|
||||
return {
|
||||
finding: data?.findingCreated,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for subscribing to new risks
|
||||
*/
|
||||
export function useRiskCreated(resourceId?: string) {
|
||||
const { data, loading, error } = useSubscription(RISK_CREATED, {
|
||||
variables: { resourceId: resourceId || null },
|
||||
})
|
||||
|
||||
return {
|
||||
risk: data?.riskCreated,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const GET_API_KEYS = gql`
|
||||
query GetApiKeys {
|
||||
apiKeys {
|
||||
id
|
||||
name
|
||||
keyPrefix
|
||||
permissions
|
||||
lastUsedAt
|
||||
expiresAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_API_KEY = gql`
|
||||
query GetApiKey($id: ID!) {
|
||||
apiKey(id: $id) {
|
||||
id
|
||||
name
|
||||
keyPrefix
|
||||
permissions
|
||||
lastUsedAt
|
||||
expiresAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CREATE_API_KEY = gql`
|
||||
mutation CreateApiKey($input: CreateApiKeyInput!) {
|
||||
createApiKey(input: $input) {
|
||||
id
|
||||
name
|
||||
key
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_API_KEY = gql`
|
||||
mutation UpdateApiKey($id: ID!, $input: UpdateApiKeyInput!) {
|
||||
updateApiKey(id: $id, input: $input) {
|
||||
id
|
||||
name
|
||||
keyPrefix
|
||||
permissions
|
||||
expiresAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const REVOKE_API_KEY = gql`
|
||||
mutation RevokeApiKey($id: ID!) {
|
||||
revokeApiKey(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const GET_SYSTEM_HEALTH = gql`
|
||||
query GetSystemHealth {
|
||||
resources(filter: { status: RUNNING }) {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
site {
|
||||
id
|
||||
name
|
||||
region
|
||||
}
|
||||
}
|
||||
sites {
|
||||
id
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_RESOURCE_UTILIZATION = gql`
|
||||
query GetResourceUtilization($resourceId: ID!, $timeRange: TimeRange!) {
|
||||
metrics(resourceId: $resourceId, metricType: CPU_USAGE, timeRange: $timeRange) {
|
||||
values {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
resource(id: $resourceId) {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COST_OVERVIEW = gql`
|
||||
query GetCostOverview($tenantId: ID!, $timeRange: TimeRange!) {
|
||||
usage(tenantId: $tenantId, timeRange: $timeRange, granularity: DAY) {
|
||||
totalCost
|
||||
currency
|
||||
byResource {
|
||||
resourceId
|
||||
resourceName
|
||||
cost
|
||||
}
|
||||
}
|
||||
costForecast(tenantId: $tenantId, timeframe: "30D", confidence: 0.95) {
|
||||
currentCost
|
||||
predictedCost
|
||||
confidence
|
||||
trend
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_BILLING_INFO = gql`
|
||||
query GetBillingInfo($tenantId: ID!) {
|
||||
invoices(tenantId: $tenantId, filter: { status: PENDING }) {
|
||||
id
|
||||
invoiceNumber
|
||||
total
|
||||
status
|
||||
dueDate
|
||||
}
|
||||
budgets(tenantId: $tenantId) {
|
||||
id
|
||||
name
|
||||
amount
|
||||
currentSpend
|
||||
remaining
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_API_USAGE = gql`
|
||||
query GetAPIUsage($timeRange: TimeRange!) {
|
||||
analyticsAPIUsage(timeRange: $timeRange) {
|
||||
totalRequests
|
||||
byEndpoint {
|
||||
endpoint
|
||||
requests
|
||||
errors
|
||||
}
|
||||
errorRate
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_DEPLOYMENTS = gql`
|
||||
query GetDeployments($filter: DeploymentFilter) {
|
||||
deployments(filter: $filter) {
|
||||
id
|
||||
name
|
||||
status
|
||||
deploymentType
|
||||
region
|
||||
createdAt
|
||||
completedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_TEST_ENVIRONMENTS = gql`
|
||||
query GetTestEnvironments {
|
||||
testEnvironments {
|
||||
id
|
||||
name
|
||||
region
|
||||
status
|
||||
resources {
|
||||
vms
|
||||
storage
|
||||
network
|
||||
}
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_API_KEYS = gql`
|
||||
query GetAPIKeys {
|
||||
apiKeys {
|
||||
id
|
||||
name
|
||||
keyPrefix
|
||||
permissions
|
||||
lastUsedAt
|
||||
expiresAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_DATA_PIPELINE = gql`
|
||||
query GetDataPipeline {
|
||||
resources(filter: { type: STORAGE }) {
|
||||
id
|
||||
name
|
||||
status
|
||||
metadata
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_INTEGRATION_STATUS = gql`
|
||||
query GetIntegrationStatus {
|
||||
sites {
|
||||
id
|
||||
name
|
||||
status
|
||||
region
|
||||
}
|
||||
omadaSites {
|
||||
id
|
||||
name
|
||||
accessPoints {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COMPLIANCE_STATUS = gql`
|
||||
query GetComplianceStatus($resourceId: ID) {
|
||||
policyViolations(filter: { resourceId: $resourceId, status: OPEN }) {
|
||||
id
|
||||
severity
|
||||
message
|
||||
policyId
|
||||
createdAt
|
||||
}
|
||||
findings(filter: { status: FAIL }) {
|
||||
id
|
||||
severity
|
||||
title
|
||||
description
|
||||
resource {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_SERVICE_ADOPTION = gql`
|
||||
query GetServiceAdoption {
|
||||
resources {
|
||||
id
|
||||
name
|
||||
type
|
||||
createdAt
|
||||
}
|
||||
deployments {
|
||||
id
|
||||
name
|
||||
deploymentType
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_RESOURCE_USAGE = gql`
|
||||
query GetResourceUsage($tenantId: ID!, $timeRange: TimeRange!) {
|
||||
usage(tenantId: $tenantId, timeRange: $timeRange, granularity: HOUR) {
|
||||
totalCost
|
||||
byResource {
|
||||
resourceId
|
||||
resourceName
|
||||
resourceType
|
||||
cost
|
||||
quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const GET_COUNTRIES = gql`
|
||||
query GetCountries($filter: CountryFilter) {
|
||||
countries(filter: $filter) {
|
||||
name
|
||||
region
|
||||
relationshipType
|
||||
priority
|
||||
cloudflareCoverage
|
||||
networkInfrastructurePriority
|
||||
notes
|
||||
coordinates {
|
||||
lat
|
||||
lng
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COUNTRY = gql`
|
||||
query GetCountry($name: String!) {
|
||||
country(name: $name) {
|
||||
name
|
||||
region
|
||||
relationshipType
|
||||
priority
|
||||
cloudflareCoverage
|
||||
networkInfrastructurePriority
|
||||
notes
|
||||
coordinates {
|
||||
lat
|
||||
lng
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_NETWORK_TOPOLOGIES = gql`
|
||||
query GetNetworkTopologies($filter: TopologyFilter) {
|
||||
networkTopologies(filter: $filter) {
|
||||
id
|
||||
nodes {
|
||||
id
|
||||
type
|
||||
label
|
||||
region
|
||||
entity
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
metadata
|
||||
}
|
||||
edges {
|
||||
id
|
||||
source
|
||||
target
|
||||
type
|
||||
metadata
|
||||
}
|
||||
region
|
||||
entity
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_NETWORK_TOPOLOGY = gql`
|
||||
query GetNetworkTopology($id: ID!) {
|
||||
networkTopology(id: $id) {
|
||||
id
|
||||
nodes {
|
||||
id
|
||||
type
|
||||
label
|
||||
region
|
||||
entity
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
metadata
|
||||
}
|
||||
edges {
|
||||
id
|
||||
source
|
||||
target
|
||||
type
|
||||
metadata
|
||||
}
|
||||
region
|
||||
entity
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COMPLIANCE_REQUIREMENTS = gql`
|
||||
query GetComplianceRequirements($filter: ComplianceFilter) {
|
||||
complianceRequirements(filter: $filter) {
|
||||
country
|
||||
region
|
||||
frameworks
|
||||
status
|
||||
requirements
|
||||
lastAuditDate
|
||||
notes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COMPLIANCE_REQUIREMENT = gql`
|
||||
query GetComplianceRequirement($country: String!) {
|
||||
complianceRequirement(country: $country) {
|
||||
country
|
||||
region
|
||||
frameworks
|
||||
status
|
||||
requirements
|
||||
lastAuditDate
|
||||
notes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_DEPLOYMENT_MILESTONES = gql`
|
||||
query GetDeploymentMilestones($filter: MilestoneFilter) {
|
||||
deploymentMilestones(filter: $filter) {
|
||||
id
|
||||
title
|
||||
region
|
||||
entity
|
||||
priority
|
||||
startDate
|
||||
endDate
|
||||
status
|
||||
dependencies
|
||||
cost
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_DEPLOYMENT_MILESTONE = gql`
|
||||
query GetDeploymentMilestone($id: ID!) {
|
||||
deploymentMilestone(id: $id) {
|
||||
id
|
||||
title
|
||||
region
|
||||
entity
|
||||
priority
|
||||
startDate
|
||||
endDate
|
||||
status
|
||||
dependencies
|
||||
cost
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COST_ESTIMATES = gql`
|
||||
query GetCostEstimates($filter: CostEstimateFilter) {
|
||||
costEstimates(filter: $filter) {
|
||||
region
|
||||
entity
|
||||
category
|
||||
monthly
|
||||
annual
|
||||
breakdown {
|
||||
compute
|
||||
storage
|
||||
network
|
||||
licenses
|
||||
personnel
|
||||
}
|
||||
currency
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_COST_ESTIMATE = gql`
|
||||
query GetCostEstimate($region: String!, $entity: String!, $category: CostCategory!) {
|
||||
costEstimate(region: $region, entity: $entity, category: $category) {
|
||||
region
|
||||
entity
|
||||
category
|
||||
monthly
|
||||
annual
|
||||
breakdown {
|
||||
compute
|
||||
storage
|
||||
network
|
||||
licenses
|
||||
personnel
|
||||
}
|
||||
currency
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_INFRASTRUCTURE_SUMMARY = gql`
|
||||
query GetInfrastructureSummary {
|
||||
infrastructureSummary {
|
||||
totalCountries
|
||||
totalRegions
|
||||
totalCost
|
||||
deploymentProgress {
|
||||
planned
|
||||
inProgress
|
||||
complete
|
||||
blocked
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_NETWORK_TOPOLOGY = gql`
|
||||
mutation UpdateNetworkTopology($id: ID!, $input: UpdateTopologyInput!) {
|
||||
updateNetworkTopology(id: $id, input: $input) {
|
||||
id
|
||||
nodes {
|
||||
id
|
||||
type
|
||||
label
|
||||
region
|
||||
entity
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
metadata
|
||||
}
|
||||
edges {
|
||||
id
|
||||
source
|
||||
target
|
||||
type
|
||||
metadata
|
||||
}
|
||||
region
|
||||
entity
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CREATE_DEPLOYMENT_MILESTONE = gql`
|
||||
mutation CreateDeploymentMilestone($input: CreateMilestoneInput!) {
|
||||
createDeploymentMilestone(input: $input) {
|
||||
id
|
||||
title
|
||||
region
|
||||
entity
|
||||
priority
|
||||
startDate
|
||||
endDate
|
||||
status
|
||||
dependencies
|
||||
cost
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_DEPLOYMENT_MILESTONE = gql`
|
||||
mutation UpdateDeploymentMilestone($id: ID!, $input: UpdateMilestoneInput!) {
|
||||
updateDeploymentMilestone(id: $id, input: $input) {
|
||||
id
|
||||
title
|
||||
region
|
||||
entity
|
||||
priority
|
||||
startDate
|
||||
endDate
|
||||
status
|
||||
dependencies
|
||||
cost
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const DELETE_DEPLOYMENT_MILESTONE = gql`
|
||||
mutation DeleteDeploymentMilestone($id: ID!) {
|
||||
deleteDeploymentMilestone(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_COMPLIANCE_REQUIREMENT = gql`
|
||||
mutation UpdateComplianceRequirement($country: String!, $input: UpdateComplianceInput!) {
|
||||
updateComplianceRequirement(country: $country, input: $input) {
|
||||
country
|
||||
region
|
||||
frameworks
|
||||
status
|
||||
requirements
|
||||
lastAuditDate
|
||||
notes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_COST_ESTIMATE = gql`
|
||||
mutation UpdateCostEstimate(
|
||||
$region: String!
|
||||
$entity: String!
|
||||
$category: CostCategory!
|
||||
$input: UpdateCostEstimateInput!
|
||||
) {
|
||||
updateCostEstimate(region: $region, entity: $entity, category: $category, input: $input) {
|
||||
region
|
||||
entity
|
||||
category
|
||||
monthly
|
||||
annual
|
||||
breakdown {
|
||||
compute
|
||||
storage
|
||||
network
|
||||
licenses
|
||||
personnel
|
||||
}
|
||||
currency
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const GET_TEST_ENVIRONMENTS = gql`
|
||||
query GetTestEnvironments {
|
||||
testEnvironments {
|
||||
id
|
||||
name
|
||||
region
|
||||
status
|
||||
resources {
|
||||
vms
|
||||
storage
|
||||
network
|
||||
}
|
||||
expiresAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_TEST_ENVIRONMENT = gql`
|
||||
query GetTestEnvironment($id: ID!) {
|
||||
testEnvironment(id: $id) {
|
||||
id
|
||||
name
|
||||
region
|
||||
status
|
||||
resources {
|
||||
vms
|
||||
storage
|
||||
network
|
||||
}
|
||||
expiresAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CREATE_TEST_ENVIRONMENT = gql`
|
||||
mutation CreateTestEnvironment($input: CreateTestEnvironmentInput!) {
|
||||
createTestEnvironment(input: $input) {
|
||||
id
|
||||
name
|
||||
region
|
||||
status
|
||||
resources {
|
||||
vms
|
||||
storage
|
||||
network
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const START_TEST_ENVIRONMENT = gql`
|
||||
mutation StartTestEnvironment($id: ID!) {
|
||||
startTestEnvironment(id: $id) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const STOP_TEST_ENVIRONMENT = gql`
|
||||
mutation StopTestEnvironment($id: ID!) {
|
||||
stopTestEnvironment(id: $id) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const DELETE_TEST_ENVIRONMENT = gql`
|
||||
mutation DeleteTestEnvironment($id: ID!) {
|
||||
deleteTestEnvironment(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,75 +1,79 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
// Subscription for resource updates
|
||||
export const RESOURCE_UPDATED = gql`
|
||||
subscription ResourceUpdated($resourceId: ID!) {
|
||||
resourceUpdated(resourceId: $resourceId) {
|
||||
export const SUBSCRIBE_TOPOlogy_CHANGES = gql`
|
||||
subscription SubscribeTopologyChanges($id: ID!) {
|
||||
topologyChanged(id: $id) {
|
||||
id
|
||||
name
|
||||
type
|
||||
health
|
||||
metadata
|
||||
region
|
||||
entity
|
||||
nodes {
|
||||
id
|
||||
type
|
||||
label
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
edges {
|
||||
id
|
||||
source
|
||||
target
|
||||
type
|
||||
}
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Subscription for metrics updates
|
||||
export const METRICS_UPDATED = gql`
|
||||
subscription MetricsUpdated($resourceId: ID!, $metricType: MetricType!) {
|
||||
metricsUpdated(resourceId: $resourceId, metricType: $metricType) {
|
||||
timestamp
|
||||
value
|
||||
labels
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Subscription for health changes
|
||||
export const HEALTH_CHANGED = gql`
|
||||
subscription HealthChanged($resourceId: ID!) {
|
||||
healthChanged(resourceId: $resourceId)
|
||||
}
|
||||
`
|
||||
|
||||
// Subscription for new findings
|
||||
export const FINDING_CREATED = gql`
|
||||
subscription FindingCreated($controlId: ID) {
|
||||
findingCreated(controlId: $controlId) {
|
||||
id
|
||||
control {
|
||||
id
|
||||
code
|
||||
name
|
||||
}
|
||||
resource {
|
||||
id
|
||||
name
|
||||
}
|
||||
export const SUBSCRIBE_COMPLIANCE_CHANGES = gql`
|
||||
subscription SubscribeComplianceChanges($country: String) {
|
||||
complianceChanged(country: $country) {
|
||||
country
|
||||
region
|
||||
frameworks
|
||||
status
|
||||
severity
|
||||
title
|
||||
description
|
||||
requirements
|
||||
lastAuditDate
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Subscription for new risks
|
||||
export const RISK_CREATED = gql`
|
||||
subscription RiskCreated($resourceId: ID) {
|
||||
riskCreated(resourceId: $resourceId) {
|
||||
export const SUBSCRIBE_MILESTONE_CHANGES = gql`
|
||||
subscription SubscribeMilestoneChanges($region: String) {
|
||||
milestoneChanged(region: $region) {
|
||||
id
|
||||
resource {
|
||||
id
|
||||
name
|
||||
}
|
||||
pillar {
|
||||
code
|
||||
name
|
||||
}
|
||||
severity
|
||||
title
|
||||
region
|
||||
entity
|
||||
priority
|
||||
startDate
|
||||
endDate
|
||||
status
|
||||
dependencies
|
||||
cost
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SUBSCRIBE_COST_CHANGES = gql`
|
||||
subscription SubscribeCostChanges($region: String) {
|
||||
costChanged(region: $region) {
|
||||
region
|
||||
entity
|
||||
category
|
||||
monthly
|
||||
annual
|
||||
breakdown {
|
||||
compute
|
||||
storage
|
||||
network
|
||||
licenses
|
||||
personnel
|
||||
}
|
||||
currency
|
||||
lastUpdated
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useCountries, useNetworkTopologies, useComplianceRequirements } from '../useInfrastructureData'
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn()
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useInfrastructureData', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useCountries', () => {
|
||||
it('should fetch countries successfully', async () => {
|
||||
const mockCountries = [
|
||||
{ name: 'Italy', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
|
||||
{ name: 'Germany', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
|
||||
]
|
||||
|
||||
;(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCountries,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCountries(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
expect(result.current.data).toEqual(mockCountries)
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/infrastructure/data/smom_countries.json')
|
||||
})
|
||||
|
||||
it('should filter countries by region', async () => {
|
||||
const mockCountries = [
|
||||
{ name: 'Italy', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
|
||||
{ name: 'Angola', region: 'Africa (Sub-Saharan)', relationshipType: 'Full Diplomatic Relations' },
|
||||
]
|
||||
|
||||
;(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockCountries,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCountries({ region: 'Europe' }), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
expect(result.current.data).toHaveLength(1)
|
||||
expect(result.current.data?.[0].name).toBe('Italy')
|
||||
})
|
||||
|
||||
it('should handle fetch errors', async () => {
|
||||
;(global.fetch as any).mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
const { result } = renderHook(() => useCountries(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
|
||||
expect(result.current.error).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useNetworkTopologies', () => {
|
||||
it('should fetch topologies successfully', async () => {
|
||||
const mockTopologies = [
|
||||
{
|
||||
id: '1',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
]
|
||||
|
||||
;(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockTopologies,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useNetworkTopologies(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
expect(result.current.topologies).toEqual(mockTopologies)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useComplianceRequirements', () => {
|
||||
it('should fetch compliance requirements successfully', async () => {
|
||||
const mockRequirements = [
|
||||
{
|
||||
country: 'Italy',
|
||||
region: 'Europe',
|
||||
frameworks: ['GDPR'],
|
||||
status: 'Compliant',
|
||||
requirements: ['Data protection'],
|
||||
},
|
||||
]
|
||||
|
||||
;(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockRequirements,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useComplianceRequirements(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
expect(result.current.requirements).toEqual(mockRequirements)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Hook for loading infrastructure data from JSON files via API route
|
||||
* Uses React Query for caching, retry logic, and state management
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type {
|
||||
Country,
|
||||
NetworkTopology,
|
||||
ComplianceRequirement,
|
||||
DeploymentMilestone,
|
||||
CostEstimate,
|
||||
InfrastructureSummary,
|
||||
} from '../types/infrastructure'
|
||||
|
||||
// Data files are served via API route
|
||||
const DATA_BASE_PATH = '/api/infrastructure/data'
|
||||
|
||||
// API response type
|
||||
interface APIResponse<T> {
|
||||
data: T[]
|
||||
metadata: {
|
||||
filename: string
|
||||
lastModified: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch JSON data from API route with error handling
|
||||
async function fetchJSONData<T>(filename: string): Promise<T[]> {
|
||||
const response = await fetch(`${DATA_BASE_PATH}/${filename}`)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.warn(`File not found: ${filename}`)
|
||||
return []
|
||||
}
|
||||
throw new Error(`Failed to load ${filename}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result: APIResponse<T> = await response.json()
|
||||
return result.data || []
|
||||
}
|
||||
|
||||
export function useCountries(filter?: {
|
||||
region?: string
|
||||
priority?: string
|
||||
relationshipType?: string
|
||||
}) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ['countries', filter],
|
||||
queryFn: () => fetchJSONData<Country>('smom_countries.json'),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
})
|
||||
|
||||
// Apply filters to the data
|
||||
const countries = data
|
||||
? data.filter((c) => {
|
||||
if (filter?.region && c.region !== filter.region) return false
|
||||
if (filter?.priority && c.priority !== filter.priority) return false
|
||||
if (filter?.relationshipType && c.relationshipType !== filter.relationshipType) return false
|
||||
return true
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
countries,
|
||||
loading: isLoading,
|
||||
error: isError ? (error instanceof Error ? error : new Error('Failed to load countries')) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useNetworkTopologies(filter?: { region?: string; entity?: string }) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ['networkTopologies', filter],
|
||||
queryFn: () => fetchJSONData<NetworkTopology>('network_topology.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
})
|
||||
|
||||
const topologies = data
|
||||
? data.filter((t) => {
|
||||
if (filter?.region && t.region !== filter.region) return false
|
||||
if (filter?.entity && t.entity !== filter.entity) return false
|
||||
return true
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
topologies,
|
||||
loading: isLoading,
|
||||
error: isError ? (error instanceof Error ? error : new Error('Failed to load topologies')) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useComplianceRequirements(filter?: {
|
||||
country?: string
|
||||
region?: string
|
||||
status?: string
|
||||
framework?: string
|
||||
}) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ['complianceRequirements', filter],
|
||||
queryFn: () => fetchJSONData<ComplianceRequirement>('compliance_requirements.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
})
|
||||
|
||||
const requirements = data
|
||||
? data.filter((r) => {
|
||||
if (filter?.country && r.country !== filter.country) return false
|
||||
if (filter?.region && r.region !== filter.region) return false
|
||||
if (filter?.status && r.status !== filter.status) return false
|
||||
if (filter?.framework && !r.frameworks.includes(filter.framework)) return false
|
||||
return true
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
requirements,
|
||||
loading: isLoading,
|
||||
error: isError
|
||||
? (error instanceof Error ? error : new Error('Failed to load compliance requirements'))
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useDeploymentMilestones(filter?: {
|
||||
region?: string
|
||||
entity?: string
|
||||
priority?: string
|
||||
status?: string
|
||||
}) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ['deploymentMilestones', filter],
|
||||
queryFn: () => fetchJSONData<DeploymentMilestone>('deployment_timeline.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
})
|
||||
|
||||
const milestones = data
|
||||
? data.filter((m) => {
|
||||
if (filter?.region && m.region !== filter.region) return false
|
||||
if (filter?.entity && m.entity !== filter.entity) return false
|
||||
if (filter?.priority && m.priority !== filter.priority) return false
|
||||
if (filter?.status && m.status !== filter.status) return false
|
||||
return true
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
milestones,
|
||||
loading: isLoading,
|
||||
error: isError
|
||||
? (error instanceof Error ? error : new Error('Failed to load milestones'))
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useCostEstimates(filter?: {
|
||||
region?: string
|
||||
entity?: string
|
||||
category?: string
|
||||
}) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ['costEstimates', filter],
|
||||
queryFn: () => fetchJSONData<CostEstimate>('cost_estimates.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
})
|
||||
|
||||
const estimates = data
|
||||
? data.filter((e) => {
|
||||
if (filter?.region && e.region !== filter.region) return false
|
||||
if (filter?.entity && e.entity !== filter.entity) return false
|
||||
if (filter?.category && e.category !== filter.category) return false
|
||||
return true
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
estimates,
|
||||
loading: isLoading,
|
||||
error: isError
|
||||
? (error instanceof Error ? error : new Error('Failed to load cost estimates'))
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useInfrastructureSummary() {
|
||||
const { data: countries, isLoading: countriesLoading } = useQuery({
|
||||
queryKey: ['countries'],
|
||||
queryFn: () => fetchJSONData<Country>('smom_countries.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const { data: milestones, isLoading: milestonesLoading } = useQuery({
|
||||
queryKey: ['deploymentMilestones'],
|
||||
queryFn: () => fetchJSONData<DeploymentMilestone>('deployment_timeline.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const { data: estimates, isLoading: estimatesLoading } = useQuery({
|
||||
queryKey: ['costEstimates'],
|
||||
queryFn: () => fetchJSONData<CostEstimate>('cost_estimates.json'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const loading = countriesLoading || milestonesLoading || estimatesLoading
|
||||
|
||||
// Calculate summary from loaded data
|
||||
const summary: InfrastructureSummary | null =
|
||||
countries && milestones && estimates
|
||||
? (() => {
|
||||
const regions = new Set(countries.map((c) => c.region))
|
||||
const totalCost = estimates.reduce((sum, e) => sum + e.annual, 0)
|
||||
|
||||
const progress = milestones.reduce(
|
||||
(acc, m) => {
|
||||
acc[m.status] = (acc[m.status] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
)
|
||||
|
||||
return {
|
||||
totalCountries: countries.length,
|
||||
totalRegions: regions.size,
|
||||
totalCost,
|
||||
deploymentProgress: {
|
||||
planned: progress.Planned || 0,
|
||||
inProgress: progress['In Progress'] || 0,
|
||||
complete: progress.Complete || 0,
|
||||
blocked: progress.Blocked || 0,
|
||||
},
|
||||
}
|
||||
})()
|
||||
: null
|
||||
|
||||
return { summary, loading, error: null }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export const supportedLanguages = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
|
||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
|
||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
|
||||
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
|
||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
|
||||
{ code: 'sw', name: 'Swahili', nativeName: 'Kiswahili' },
|
||||
] as const;
|
||||
|
||||
export type LanguageCode = typeof supportedLanguages[number]['code'];
|
||||
|
||||
export const defaultLanguage: LanguageCode = 'en';
|
||||
|
||||
export function getLanguageName(code: LanguageCode): string {
|
||||
return supportedLanguages.find(lang => lang.code === code)?.name || 'English';
|
||||
}
|
||||
|
||||
export function getNativeLanguageName(code: LanguageCode): string {
|
||||
return supportedLanguages.find(lang => lang.code === code)?.nativeName || 'English';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
import { LanguageCode } from './config';
|
||||
|
||||
export type TranslationKey =
|
||||
| 'nav.products'
|
||||
| 'nav.solutions'
|
||||
| 'nav.developers'
|
||||
| 'nav.partners'
|
||||
| 'nav.company'
|
||||
| 'nav.support'
|
||||
| 'nav.signIn'
|
||||
| 'hero.title'
|
||||
| 'hero.subtitle'
|
||||
| 'footer.copyright'
|
||||
| 'common.loading'
|
||||
| 'common.error'
|
||||
| 'common.save'
|
||||
| 'common.cancel'
|
||||
| 'common.delete'
|
||||
| 'common.edit';
|
||||
|
||||
export const translations: Record<LanguageCode, Record<TranslationKey, string>> = {
|
||||
en: {
|
||||
'nav.products': 'Products',
|
||||
'nav.solutions': 'Solutions',
|
||||
'nav.developers': 'Developers',
|
||||
'nav.partners': 'Partners',
|
||||
'nav.company': 'Company',
|
||||
'nav.support': 'Support',
|
||||
'nav.signIn': 'Sign In',
|
||||
'hero.title': "Sankofa's Phoenix Nexus Cloud",
|
||||
'hero.subtitle': 'The sovereign cloud born of fire and ancestral wisdom.',
|
||||
'footer.copyright': 'All rights reserved',
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'An error occurred',
|
||||
'common.save': 'Save',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.delete': 'Delete',
|
||||
'common.edit': 'Edit',
|
||||
},
|
||||
es: {
|
||||
'nav.products': 'Productos',
|
||||
'nav.solutions': 'Soluciones',
|
||||
'nav.developers': 'Desarrolladores',
|
||||
'nav.partners': 'Socios',
|
||||
'nav.company': 'Empresa',
|
||||
'nav.support': 'Soporte',
|
||||
'nav.signIn': 'Iniciar sesión',
|
||||
'hero.title': 'Nube Nexus Phoenix de Sankofa',
|
||||
'hero.subtitle': 'La nube soberana nacida del fuego y la sabiduría ancestral.',
|
||||
'footer.copyright': 'Todos los derechos reservados',
|
||||
'common.loading': 'Cargando...',
|
||||
'common.error': 'Ocurrió un error',
|
||||
'common.save': 'Guardar',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.delete': 'Eliminar',
|
||||
'common.edit': 'Editar',
|
||||
},
|
||||
fr: {
|
||||
'nav.products': 'Produits',
|
||||
'nav.solutions': 'Solutions',
|
||||
'nav.developers': 'Développeurs',
|
||||
'nav.partners': 'Partenaires',
|
||||
'nav.company': 'Entreprise',
|
||||
'nav.support': 'Support',
|
||||
'nav.signIn': 'Se connecter',
|
||||
'hero.title': 'Cloud Nexus Phoenix de Sankofa',
|
||||
'hero.subtitle': 'Le cloud souverain né du feu et de la sagesse ancestrale.',
|
||||
'footer.copyright': 'Tous droits réservés',
|
||||
'common.loading': 'Chargement...',
|
||||
'common.error': 'Une erreur est survenue',
|
||||
'common.save': 'Enregistrer',
|
||||
'common.cancel': 'Annuler',
|
||||
'common.delete': 'Supprimer',
|
||||
'common.edit': 'Modifier',
|
||||
},
|
||||
de: {
|
||||
'nav.products': 'Produkte',
|
||||
'nav.solutions': 'Lösungen',
|
||||
'nav.developers': 'Entwickler',
|
||||
'nav.partners': 'Partner',
|
||||
'nav.company': 'Unternehmen',
|
||||
'nav.support': 'Support',
|
||||
'nav.signIn': 'Anmelden',
|
||||
'hero.title': 'Sankofas Phoenix Nexus Cloud',
|
||||
'hero.subtitle': 'Die souveräne Cloud, geboren aus Feuer und ancestraler Weisheit.',
|
||||
'footer.copyright': 'Alle Rechte vorbehalten',
|
||||
'common.loading': 'Laden...',
|
||||
'common.error': 'Ein Fehler ist aufgetreten',
|
||||
'common.save': 'Speichern',
|
||||
'common.cancel': 'Abbrechen',
|
||||
'common.delete': 'Löschen',
|
||||
'common.edit': 'Bearbeiten',
|
||||
},
|
||||
zh: {
|
||||
'nav.products': '产品',
|
||||
'nav.solutions': '解决方案',
|
||||
'nav.developers': '开发者',
|
||||
'nav.partners': '合作伙伴',
|
||||
'nav.company': '公司',
|
||||
'nav.support': '支持',
|
||||
'nav.signIn': '登录',
|
||||
'hero.title': 'Sankofa 凤凰 Nexus 云',
|
||||
'hero.subtitle': '由火和祖先智慧诞生的主权云。',
|
||||
'footer.copyright': '版权所有',
|
||||
'common.loading': '加载中...',
|
||||
'common.error': '发生错误',
|
||||
'common.save': '保存',
|
||||
'common.cancel': '取消',
|
||||
'common.delete': '删除',
|
||||
'common.edit': '编辑',
|
||||
},
|
||||
ja: {
|
||||
'nav.products': '製品',
|
||||
'nav.solutions': 'ソリューション',
|
||||
'nav.developers': '開発者',
|
||||
'nav.partners': 'パートナー',
|
||||
'nav.company': '会社',
|
||||
'nav.support': 'サポート',
|
||||
'nav.signIn': 'サインイン',
|
||||
'hero.title': 'Sankofaのフェニックスネクサスクラウド',
|
||||
'hero.subtitle': '火と祖先の知恵から生まれた主権クラウド。',
|
||||
'footer.copyright': '全著作権所有',
|
||||
'common.loading': '読み込み中...',
|
||||
'common.error': 'エラーが発生しました',
|
||||
'common.save': '保存',
|
||||
'common.cancel': 'キャンセル',
|
||||
'common.delete': '削除',
|
||||
'common.edit': '編集',
|
||||
},
|
||||
pt: {
|
||||
'nav.products': 'Produtos',
|
||||
'nav.solutions': 'Soluções',
|
||||
'nav.developers': 'Desenvolvedores',
|
||||
'nav.partners': 'Parceiros',
|
||||
'nav.company': 'Empresa',
|
||||
'nav.support': 'Suporte',
|
||||
'nav.signIn': 'Entrar',
|
||||
'hero.title': 'Nuvem Nexus Phoenix da Sankofa',
|
||||
'hero.subtitle': 'A nuvem soberana nascida do fogo e da sabedoria ancestral.',
|
||||
'footer.copyright': 'Todos os direitos reservados',
|
||||
'common.loading': 'Carregando...',
|
||||
'common.error': 'Ocorreu um erro',
|
||||
'common.save': 'Salvar',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.delete': 'Excluir',
|
||||
'common.edit': 'Editar',
|
||||
},
|
||||
ar: {
|
||||
'nav.products': 'المنتجات',
|
||||
'nav.solutions': 'الحلول',
|
||||
'nav.developers': 'المطورون',
|
||||
'nav.partners': 'الشركاء',
|
||||
'nav.company': 'الشركة',
|
||||
'nav.support': 'الدعم',
|
||||
'nav.signIn': 'تسجيل الدخول',
|
||||
'hero.title': 'سحابة Sankofa Phoenix Nexus',
|
||||
'hero.subtitle': 'السحابة السيادية المولودة من النار والحكمة الأسلافية.',
|
||||
'footer.copyright': 'جميع الحقوق محفوظة',
|
||||
'common.loading': 'جاري التحميل...',
|
||||
'common.error': 'حدث خطأ',
|
||||
'common.save': 'حفظ',
|
||||
'common.cancel': 'إلغاء',
|
||||
'common.delete': 'حذف',
|
||||
'common.edit': 'تعديل',
|
||||
},
|
||||
hi: {
|
||||
'nav.products': 'उत्पाद',
|
||||
'nav.solutions': 'समाधान',
|
||||
'nav.developers': 'डेवलपर्स',
|
||||
'nav.partners': 'साझेदार',
|
||||
'nav.company': 'कंपनी',
|
||||
'nav.support': 'सहायता',
|
||||
'nav.signIn': 'साइन इन करें',
|
||||
'hero.title': 'Sankofa का Phoenix Nexus Cloud',
|
||||
'hero.subtitle': 'अग्नि और पूर्वजों की बुद्धि से जन्मी संप्रभु क्लाउड।',
|
||||
'footer.copyright': 'सभी अधिकार सुरक्षित',
|
||||
'common.loading': 'लोड हो रहा है...',
|
||||
'common.error': 'एक त्रुटि हुई',
|
||||
'common.save': 'सहेजें',
|
||||
'common.cancel': 'रद्द करें',
|
||||
'common.delete': 'हटाएं',
|
||||
'common.edit': 'संपादित करें',
|
||||
},
|
||||
sw: {
|
||||
'nav.products': 'Bidhaa',
|
||||
'nav.solutions': 'Suluhisho',
|
||||
'nav.developers': 'Wasanidi',
|
||||
'nav.partners': 'Washirika',
|
||||
'nav.company': 'Kampuni',
|
||||
'nav.support': 'Msaada',
|
||||
'nav.signIn': 'Ingia',
|
||||
'hero.title': 'Wingu la Sankofa Phoenix Nexus',
|
||||
'hero.subtitle': 'Wingu la kujitegemea linalozaliwa na moto na hekima ya mababu.',
|
||||
'footer.copyright': 'Haki zote zimehifadhiwa',
|
||||
'common.loading': 'Inapakia...',
|
||||
'common.error': 'Hitilafu imetokea',
|
||||
'common.save': 'Hifadhi',
|
||||
'common.cancel': 'Ghairi',
|
||||
'common.delete': 'Futa',
|
||||
'common.edit': 'Hariri',
|
||||
},
|
||||
};
|
||||
|
||||
export function getTranslation(key: TranslationKey, language: LanguageCode): string {
|
||||
return translations[language]?.[key] || translations.en[key] || key;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Mock API stubs for frontend development
|
||||
* These can be used during parallel development before backend is ready
|
||||
*/
|
||||
|
||||
export const mockResources = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'web-server-01',
|
||||
type: 'VM',
|
||||
status: 'RUNNING',
|
||||
site: {
|
||||
id: 'site-1',
|
||||
name: 'US East Primary Site',
|
||||
region: 'us-east-1',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
metadata: {
|
||||
cpu: 4,
|
||||
memory: 8192,
|
||||
disk: 100,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'db-server-01',
|
||||
type: 'VM',
|
||||
status: 'RUNNING',
|
||||
site: {
|
||||
id: 'site-1',
|
||||
name: 'US East Primary Site',
|
||||
region: 'us-east-1',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
metadata: {
|
||||
cpu: 8,
|
||||
memory: 16384,
|
||||
disk: 500,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
export const mockSites = [
|
||||
{
|
||||
id: 'site-1',
|
||||
name: 'US East Primary Site',
|
||||
region: 'us-east-1',
|
||||
status: 'ACTIVE',
|
||||
resources: mockResources,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'site-2',
|
||||
name: 'EU Central Primary Site',
|
||||
region: 'eu-central-1',
|
||||
status: 'ACTIVE',
|
||||
resources: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
export const mockResourceInventory = [
|
||||
{
|
||||
id: 'inv-1',
|
||||
resourceType: 'virtual_machine',
|
||||
provider: 'PROXMOX',
|
||||
providerId: 'vm-101',
|
||||
providerResourceId: 'proxmox://cluster1/vm/101',
|
||||
name: 'web-server-01',
|
||||
region: 'us-east-1',
|
||||
site: mockSites[0],
|
||||
metadata: {
|
||||
cpu: 4,
|
||||
memory: 8192,
|
||||
},
|
||||
tags: ['web', 'production'],
|
||||
discoveredAt: new Date().toISOString(),
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
export const mockMetrics = {
|
||||
resource: mockResourceInventory[0],
|
||||
metricType: 'CPU_USAGE',
|
||||
values: Array.from({ length: 60 }, (_, i) => ({
|
||||
timestamp: new Date(Date.now() - (60 - i) * 60000).toISOString(),
|
||||
value: Math.random() * 100,
|
||||
labels: {},
|
||||
})),
|
||||
timeRange: {
|
||||
start: new Date(Date.now() - 3600000).toISOString(),
|
||||
end: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
export const mockPillars = [
|
||||
{ id: '1', code: 'SECURITY', name: 'Security', description: 'Security pillar' },
|
||||
{ id: '2', code: 'RELIABILITY', name: 'Reliability', description: 'Reliability pillar' },
|
||||
{ id: '3', code: 'COST_OPTIMIZATION', name: 'Cost Optimization', description: 'Cost pillar' },
|
||||
{ id: '4', code: 'PERFORMANCE_EFFICIENCY', name: 'Performance Efficiency', description: 'Performance pillar' },
|
||||
{ id: '5', code: 'OPERATIONAL_EXCELLENCE', name: 'Operational Excellence', description: 'Operations pillar' },
|
||||
{ id: '6', code: 'SUSTAINABILITY', name: 'Sustainability', description: 'Sustainability pillar' },
|
||||
]
|
||||
|
||||
export const mockFindings = [
|
||||
{
|
||||
id: 'f1',
|
||||
control: {
|
||||
id: 'c1',
|
||||
code: 'SEC-001',
|
||||
name: 'Encryption at Rest',
|
||||
pillar: mockPillars[0],
|
||||
},
|
||||
resource: mockResourceInventory[0],
|
||||
status: 'PASS',
|
||||
severity: 'LOW',
|
||||
title: 'Encryption enabled',
|
||||
description: 'All storage is encrypted',
|
||||
recommendation: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
export const mockRegions = [
|
||||
{
|
||||
id: 'r1',
|
||||
name: 'US East',
|
||||
code: 'us-east-1',
|
||||
country: 'United States',
|
||||
coordinates: { latitude: 39.8283, longitude: -98.5795 },
|
||||
sites: [mockSites[0]],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Mock GraphQL query responses
|
||||
*/
|
||||
export const mockGraphQLResponses = {
|
||||
resources: () => ({ data: { resources: mockResources } }),
|
||||
sites: () => ({ data: { sites: mockSites } }),
|
||||
resourceInventory: () => ({ data: { resourceInventory: mockResourceInventory } }),
|
||||
metrics: () => ({ data: { metrics: mockMetrics } }),
|
||||
pillars: () => ({ data: { pillars: mockPillars } }),
|
||||
findings: () => ({ data: { findings: mockFindings } }),
|
||||
regions: () => ({ data: { regions: mockRegions } }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { auditLogService, type AuditLogEntry } from '../auditLog'
|
||||
|
||||
describe('auditLogService', () => {
|
||||
beforeEach(() => {
|
||||
auditLogService.clear()
|
||||
})
|
||||
|
||||
it('should log an entry', () => {
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: 'test-id',
|
||||
entityName: 'Test Entity',
|
||||
})
|
||||
|
||||
const logs = auditLogService.getLogs()
|
||||
expect(logs).toHaveLength(1)
|
||||
expect(logs[0].action).toBe('create')
|
||||
expect(logs[0].entityType).toBe('compliance')
|
||||
expect(logs[0].entityId).toBe('test-id')
|
||||
expect(logs[0].entityName).toBe('Test Entity')
|
||||
expect(logs[0].id).toBeDefined()
|
||||
expect(logs[0].timestamp).toBeDefined()
|
||||
})
|
||||
|
||||
it('should filter logs by action', () => {
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: '1',
|
||||
entityName: 'Entity 1',
|
||||
})
|
||||
|
||||
auditLogService.log({
|
||||
action: 'update',
|
||||
entityType: 'compliance',
|
||||
entityId: '2',
|
||||
entityName: 'Entity 2',
|
||||
})
|
||||
|
||||
const createLogs = auditLogService.getLogs({ action: 'create' })
|
||||
expect(createLogs).toHaveLength(1)
|
||||
expect(createLogs[0].action).toBe('create')
|
||||
})
|
||||
|
||||
it('should filter logs by entity type', () => {
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: '1',
|
||||
entityName: 'Entity 1',
|
||||
})
|
||||
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'milestone',
|
||||
entityId: '2',
|
||||
entityName: 'Entity 2',
|
||||
})
|
||||
|
||||
const complianceLogs = auditLogService.getLogs({ entityType: 'compliance' })
|
||||
expect(complianceLogs).toHaveLength(1)
|
||||
expect(complianceLogs[0].entityType).toBe('compliance')
|
||||
})
|
||||
|
||||
it('should filter logs by date range', () => {
|
||||
const now = new Date()
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
||||
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: '1',
|
||||
entityName: 'Entity 1',
|
||||
})
|
||||
|
||||
const logs = auditLogService.getLogs({
|
||||
startDate: yesterday.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
})
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should get logs by entity', () => {
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: 'test-id',
|
||||
entityName: 'Test Entity',
|
||||
})
|
||||
|
||||
auditLogService.log({
|
||||
action: 'update',
|
||||
entityType: 'compliance',
|
||||
entityId: 'test-id',
|
||||
entityName: 'Test Entity',
|
||||
})
|
||||
|
||||
const entityLogs = auditLogService.getLogsByEntity('compliance', 'test-id')
|
||||
expect(entityLogs).toHaveLength(2)
|
||||
expect(entityLogs.every((log) => log.entityId === 'test-id')).toBe(true)
|
||||
})
|
||||
|
||||
it('should limit log history size', () => {
|
||||
// Add more than maxLogs entries
|
||||
for (let i = 0; i < 10001; i++) {
|
||||
auditLogService.log({
|
||||
action: 'create',
|
||||
entityType: 'compliance',
|
||||
entityId: `id-${i}`,
|
||||
entityName: `Entity ${i}`,
|
||||
})
|
||||
}
|
||||
|
||||
const logs = auditLogService.getLogs()
|
||||
expect(logs.length).toBeLessThanOrEqual(10000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { versionControlService, type Version } from '../versionControl'
|
||||
|
||||
const mockTopology = {
|
||||
id: 'test-topology',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
nodes: [{ id: 'node-1', type: 'vm', label: 'VM 1', position: { x: 0, y: 0 } }],
|
||||
edges: [],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
describe('versionControlService', () => {
|
||||
beforeEach(() => {
|
||||
// Clear versions (would need to add clear method or reset)
|
||||
})
|
||||
|
||||
it('should save a version', () => {
|
||||
const version = versionControlService.saveVersion(mockTopology, null)
|
||||
|
||||
expect(version).toBeDefined()
|
||||
expect(version.entityId).toBe('test-topology')
|
||||
expect(version.entityType).toBe('topology')
|
||||
expect(version.version).toBe(1)
|
||||
expect(version.data).toEqual(mockTopology)
|
||||
expect(version.changes).toHaveLength(0) // No previous version
|
||||
})
|
||||
|
||||
it('should track changes between versions', () => {
|
||||
const v1 = { ...mockTopology, region: 'Europe' }
|
||||
const v2 = { ...mockTopology, region: 'Asia' }
|
||||
|
||||
versionControlService.saveVersion(v1, null)
|
||||
const version2 = versionControlService.saveVersion(v2, v1)
|
||||
|
||||
expect(version2.changes.length).toBeGreaterThan(0)
|
||||
const regionChange = version2.changes.find((c) => c.field === 'region')
|
||||
expect(regionChange).toBeDefined()
|
||||
expect(regionChange?.before).toBe('Europe')
|
||||
expect(regionChange?.after).toBe('Asia')
|
||||
})
|
||||
|
||||
it('should get versions for an entity', () => {
|
||||
versionControlService.saveVersion(mockTopology, null)
|
||||
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
|
||||
|
||||
const versions = versionControlService.getVersions('test-topology')
|
||||
expect(versions).toHaveLength(2)
|
||||
expect(versions[0].version).toBe(1)
|
||||
expect(versions[1].version).toBe(2)
|
||||
})
|
||||
|
||||
it('should get specific version', () => {
|
||||
versionControlService.saveVersion(mockTopology, null)
|
||||
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
|
||||
|
||||
const version = versionControlService.getVersion('test-topology', 1)
|
||||
expect(version).toBeDefined()
|
||||
expect(version?.version).toBe(1)
|
||||
expect(version?.data.region).toBe('Europe')
|
||||
})
|
||||
|
||||
it('should get latest version', () => {
|
||||
versionControlService.saveVersion(mockTopology, null)
|
||||
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
|
||||
|
||||
const latest = versionControlService.getLatestVersion('test-topology')
|
||||
expect(latest).toBeDefined()
|
||||
expect(latest?.version).toBe(2)
|
||||
expect(latest?.data.region).toBe('Asia')
|
||||
})
|
||||
|
||||
it('should compare versions', () => {
|
||||
const v1 = { ...mockTopology, region: 'Europe', nodes: [] }
|
||||
const v2 = { ...mockTopology, region: 'Asia', nodes: [{ id: 'new-node' }] }
|
||||
|
||||
versionControlService.saveVersion(v1, null)
|
||||
versionControlService.saveVersion(v2, v1)
|
||||
|
||||
const comparison = versionControlService.compareVersions('test-topology', 1, 2)
|
||||
|
||||
expect(comparison.modified.length).toBeGreaterThan(0)
|
||||
const regionChange = comparison.modified.find((c) => c.field === 'region')
|
||||
expect(regionChange).toBeDefined()
|
||||
})
|
||||
|
||||
it('should restore a version', () => {
|
||||
const v1 = { ...mockTopology, region: 'Europe' }
|
||||
versionControlService.saveVersion(v1, null)
|
||||
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, v1)
|
||||
|
||||
const restored = versionControlService.restoreVersion('test-topology', 1)
|
||||
expect(restored).toBeDefined()
|
||||
expect((restored as any).region).toBe('Europe')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ComplianceRequirement, DeploymentMilestone, CostEstimate, NetworkTopology } from '../types/infrastructure'
|
||||
|
||||
export type AuditAction =
|
||||
| 'create'
|
||||
| 'update'
|
||||
| 'delete'
|
||||
| 'export'
|
||||
| 'import'
|
||||
| 'backup'
|
||||
| 'restore'
|
||||
|
||||
export type AuditEntityType =
|
||||
| 'compliance'
|
||||
| 'milestone'
|
||||
| 'cost'
|
||||
| 'topology'
|
||||
| 'backup'
|
||||
| 'import'
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: string
|
||||
timestamp: string
|
||||
userId?: string
|
||||
action: AuditAction
|
||||
entityType: AuditEntityType
|
||||
entityId: string
|
||||
entityName: string
|
||||
changes?: {
|
||||
before?: any
|
||||
after?: any
|
||||
}
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
class AuditLogService {
|
||||
private logs: AuditLogEntry[] = []
|
||||
private maxLogs = 10000
|
||||
|
||||
log(entry: Omit<AuditLogEntry, 'id' | 'timestamp'>): void {
|
||||
const logEntry: AuditLogEntry = {
|
||||
...entry,
|
||||
id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
this.logs.push(logEntry)
|
||||
|
||||
// Keep only the last maxLogs entries
|
||||
if (this.logs.length > this.maxLogs) {
|
||||
this.logs = this.logs.slice(-this.maxLogs)
|
||||
}
|
||||
|
||||
// In production, also persist to database or external service
|
||||
this.persistLog(logEntry)
|
||||
}
|
||||
|
||||
private async persistLog(entry: AuditLogEntry): Promise<void> {
|
||||
try {
|
||||
// Save to JSON file (in production, use database)
|
||||
if (typeof window === 'undefined') {
|
||||
const fs = await import('fs/promises')
|
||||
const path = await import('path')
|
||||
const logDir = path.join(process.cwd(), 'docs/infrastructure/audit-logs')
|
||||
const logFile = path.join(logDir, `audit-${new Date().toISOString().split('T')[0]}.json`)
|
||||
|
||||
try {
|
||||
await fs.mkdir(logDir, { recursive: true })
|
||||
let existingLogs: AuditLogEntry[] = []
|
||||
try {
|
||||
const content = await fs.readFile(logFile, 'utf-8')
|
||||
existingLogs = JSON.parse(content)
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
existingLogs.push(entry)
|
||||
await fs.writeFile(logFile, JSON.stringify(existingLogs, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Failed to persist audit log:', error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to persist audit log:', error)
|
||||
}
|
||||
}
|
||||
|
||||
getLogs(filters?: {
|
||||
action?: AuditAction
|
||||
entityType?: AuditEntityType
|
||||
entityId?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}): AuditLogEntry[] {
|
||||
let filtered = [...this.logs]
|
||||
|
||||
if (filters?.action) {
|
||||
filtered = filtered.filter((log) => log.action === filters.action)
|
||||
}
|
||||
if (filters?.entityType) {
|
||||
filtered = filtered.filter((log) => log.entityType === filters.entityType)
|
||||
}
|
||||
if (filters?.entityId) {
|
||||
filtered = filtered.filter((log) => log.entityId === filters.entityId)
|
||||
}
|
||||
if (filters?.startDate) {
|
||||
filtered = filtered.filter((log) => log.timestamp >= filters.startDate!)
|
||||
}
|
||||
if (filters?.endDate) {
|
||||
filtered = filtered.filter((log) => log.timestamp <= filters.endDate!)
|
||||
}
|
||||
|
||||
return filtered.sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
)
|
||||
}
|
||||
|
||||
getLogsByEntity(entityType: AuditEntityType, entityId: string): AuditLogEntry[] {
|
||||
return this.getLogs({ entityType, entityId })
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.logs = []
|
||||
}
|
||||
}
|
||||
|
||||
export const auditLogService = new AuditLogService()
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { NetworkTopology, ComplianceRequirement, DeploymentMilestone, CostEstimate } from '../types/infrastructure'
|
||||
|
||||
export type VersionedEntity = NetworkTopology | ComplianceRequirement | DeploymentMilestone | CostEstimate
|
||||
|
||||
export interface Version {
|
||||
id: string
|
||||
entityId: string
|
||||
entityType: string
|
||||
version: number
|
||||
timestamp: string
|
||||
userId?: string
|
||||
data: VersionedEntity
|
||||
changes: {
|
||||
field: string
|
||||
before: any
|
||||
after: any
|
||||
}[]
|
||||
comment?: string
|
||||
}
|
||||
|
||||
class VersionControlService {
|
||||
private versions: Map<string, Version[]> = new Map()
|
||||
private maxVersionsPerEntity = 100
|
||||
|
||||
saveVersion(
|
||||
entity: VersionedEntity,
|
||||
previousVersion: VersionedEntity | null,
|
||||
userId?: string,
|
||||
comment?: string
|
||||
): Version {
|
||||
const entityId = (entity as any).id || (entity as any).country || 'unknown'
|
||||
const entityType = this.getEntityType(entity)
|
||||
const versions = this.versions.get(entityId) || []
|
||||
const versionNumber = versions.length + 1
|
||||
|
||||
const changes = previousVersion
|
||||
? this.calculateChanges(previousVersion.data, entity)
|
||||
: []
|
||||
|
||||
const version: Version = {
|
||||
id: `version-${entityId}-${versionNumber}`,
|
||||
entityId,
|
||||
entityType,
|
||||
version: versionNumber,
|
||||
timestamp: new Date().toISOString(),
|
||||
userId,
|
||||
data: JSON.parse(JSON.stringify(entity)), // Deep clone
|
||||
changes,
|
||||
comment,
|
||||
}
|
||||
|
||||
versions.push(version)
|
||||
|
||||
// Keep only the last maxVersionsPerEntity versions
|
||||
if (versions.length > this.maxVersionsPerEntity) {
|
||||
versions.shift()
|
||||
}
|
||||
|
||||
this.versions.set(entityId, versions)
|
||||
this.persistVersion(version)
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
private getEntityType(entity: VersionedEntity): string {
|
||||
if ('nodes' in entity) return 'topology'
|
||||
if ('country' in entity) return 'compliance'
|
||||
if ('title' in entity && 'startDate' in entity) return 'milestone'
|
||||
if ('category' in entity && 'monthly' in entity) return 'cost'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
private calculateChanges(before: VersionedEntity, after: VersionedEntity): Version['changes'] {
|
||||
const changes: Version['changes'] = []
|
||||
const beforeObj = before as any
|
||||
const afterObj = after as any
|
||||
|
||||
const allKeys = new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)])
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key === 'id' || key === 'lastUpdated') continue
|
||||
|
||||
const beforeVal = beforeObj[key]
|
||||
const afterVal = afterObj[key]
|
||||
|
||||
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
|
||||
changes.push({
|
||||
field: key,
|
||||
before: beforeVal,
|
||||
after: afterVal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
getVersions(entityId: string): Version[] {
|
||||
return this.versions.get(entityId) || []
|
||||
}
|
||||
|
||||
getVersion(entityId: string, versionNumber: number): Version | null {
|
||||
const versions = this.getVersions(entityId)
|
||||
return versions.find((v) => v.version === versionNumber) || null
|
||||
}
|
||||
|
||||
getLatestVersion(entityId: string): Version | null {
|
||||
const versions = this.getVersions(entityId)
|
||||
return versions[versions.length - 1] || null
|
||||
}
|
||||
|
||||
compareVersions(entityId: string, version1: number, version2: number): {
|
||||
added: Version['changes']
|
||||
removed: Version['changes']
|
||||
modified: Version['changes']
|
||||
} {
|
||||
const v1 = this.getVersion(entityId, version1)
|
||||
const v2 = this.getVersion(entityId, version2)
|
||||
|
||||
if (!v1 || !v2) {
|
||||
return { added: [], removed: [], modified: [] }
|
||||
}
|
||||
|
||||
const added: Version['changes'] = []
|
||||
const removed: Version['changes'] = []
|
||||
const modified: Version['changes'] = []
|
||||
|
||||
const v1Data = v1.data as any
|
||||
const v2Data = v2.data as any
|
||||
const allKeys = new Set([...Object.keys(v1Data), ...Object.keys(v2Data)])
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key === 'id' || key === 'lastUpdated') continue
|
||||
|
||||
const v1Val = v1Data[key]
|
||||
const v2Val = v2Data[key]
|
||||
|
||||
if (v1Val === undefined && v2Val !== undefined) {
|
||||
added.push({ field: key, before: undefined, after: v2Val })
|
||||
} else if (v1Val !== undefined && v2Val === undefined) {
|
||||
removed.push({ field: key, before: v1Val, after: undefined })
|
||||
} else if (JSON.stringify(v1Val) !== JSON.stringify(v2Val)) {
|
||||
modified.push({ field: key, before: v1Val, after: v2Val })
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, modified }
|
||||
}
|
||||
|
||||
restoreVersion(entityId: string, versionNumber: number): VersionedEntity | null {
|
||||
const version = this.getVersion(entityId, versionNumber)
|
||||
if (!version) return null
|
||||
|
||||
return JSON.parse(JSON.stringify(version.data)) // Deep clone
|
||||
}
|
||||
|
||||
private async persistVersion(version: Version): Promise<void> {
|
||||
try {
|
||||
if (typeof window === 'undefined') {
|
||||
const fs = await import('fs/promises')
|
||||
const path = await import('path')
|
||||
const versionDir = path.join(process.cwd(), 'docs/infrastructure/versions')
|
||||
const versionFile = path.join(versionDir, `${version.entityId}-versions.json`)
|
||||
|
||||
try {
|
||||
await fs.mkdir(versionDir, { recursive: true })
|
||||
let existingVersions: Version[] = []
|
||||
try {
|
||||
const content = await fs.readFile(versionFile, 'utf-8')
|
||||
existingVersions = JSON.parse(content)
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
existingVersions.push(version)
|
||||
// Keep only last maxVersionsPerEntity
|
||||
if (existingVersions.length > this.maxVersionsPerEntity) {
|
||||
existingVersions = existingVersions.slice(-this.maxVersionsPerEntity)
|
||||
}
|
||||
await fs.writeFile(versionFile, JSON.stringify(existingVersions, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Failed to persist version:', error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to persist version:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const versionControlService = new VersionControlService()
|
||||
|
||||
+22
-20
@@ -1,8 +1,8 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React from 'react'
|
||||
import { render, RenderOptions } from '@testing-library/react'
|
||||
import { ApolloProvider } from '@apollo/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { apolloClient } from './graphql/client'
|
||||
import { ApolloProvider } from '@apollo/client'
|
||||
import { apolloClient } from '../graphql/client'
|
||||
|
||||
// Create a test query client
|
||||
const createTestQueryClient = () =>
|
||||
@@ -15,23 +15,25 @@ const createTestQueryClient = () =>
|
||||
},
|
||||
})
|
||||
|
||||
interface AllTheProvidersProps {
|
||||
children: React.ReactNode
|
||||
// Custom render function with providers
|
||||
export function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
{
|
||||
queryClient = createTestQueryClient(),
|
||||
...renderOptions
|
||||
}: RenderOptions & { queryClient?: QueryClient } = {}
|
||||
) {
|
||||
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return { ...render(ui, { wrapper: Wrapper, ...renderOptions }), queryClient }
|
||||
}
|
||||
|
||||
const AllTheProviders = ({ children }: AllTheProvidersProps) => {
|
||||
const queryClient = createTestQueryClient()
|
||||
|
||||
return (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) =>
|
||||
render(ui, { wrapper: AllTheProviders, ...options })
|
||||
|
||||
// Re-export everything
|
||||
export * from '@testing-library/react'
|
||||
export { customRender as render }
|
||||
|
||||
export { renderWithProviders as render }
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* TypeScript type definitions for infrastructure documentation
|
||||
*/
|
||||
|
||||
export type Region = 'Africa (Sub-Saharan)' | 'Middle East & North Africa' | 'Americas' | 'Asia-Pacific' | 'Europe'
|
||||
export type RelationshipType = 'Full Diplomatic Relations' | 'Official (Non-Diplomatic)' | 'Ambassador Level' | 'Full Diplomatic Relations (Special Mission)'
|
||||
export type Priority = 'Critical' | 'High' | 'Medium' | 'Low'
|
||||
export type ComplianceStatus = 'Compliant' | 'Partial' | 'Pending' | 'Non-Compliant'
|
||||
export type DeploymentStatus = 'Planned' | 'In Progress' | 'Complete' | 'Blocked'
|
||||
export type CostCategory = 'Infrastructure' | 'Network' | 'Compliance' | 'Operations'
|
||||
export type TopologyNodeType = 'region' | 'datacenter' | 'tunnel' | 'vm' | 'service'
|
||||
export type TopologyEdgeType = 'tunnel' | 'peering' | 'network-route'
|
||||
|
||||
export interface Country {
|
||||
name: string
|
||||
region: Region
|
||||
relationshipType: RelationshipType
|
||||
priority: Priority
|
||||
cloudflareCoverage: boolean
|
||||
networkInfrastructurePriority: string
|
||||
notes?: string
|
||||
coordinates?: { lat: number; lng: number }
|
||||
}
|
||||
|
||||
export interface TopologyNode {
|
||||
id: string
|
||||
type: TopologyNodeType
|
||||
label: string
|
||||
region: string
|
||||
entity: string
|
||||
position: { x: number; y: number }
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface TopologyEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
type: TopologyEdgeType
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface NetworkTopology {
|
||||
nodes: TopologyNode[]
|
||||
edges: TopologyEdge[]
|
||||
region: string
|
||||
entity: string
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export interface ComplianceRequirement {
|
||||
country: string
|
||||
region: Region
|
||||
frameworks: string[]
|
||||
status: ComplianceStatus
|
||||
requirements: string[]
|
||||
lastAuditDate?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface DeploymentMilestone {
|
||||
id: string
|
||||
title: string
|
||||
region: string
|
||||
entity: string
|
||||
priority: Priority
|
||||
startDate: string
|
||||
endDate: string
|
||||
status: DeploymentStatus
|
||||
dependencies?: string[]
|
||||
cost?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface CostEstimate {
|
||||
region: string
|
||||
entity: string
|
||||
category: CostCategory
|
||||
monthly: number
|
||||
annual: number
|
||||
breakdown: {
|
||||
compute?: number
|
||||
storage?: number
|
||||
network?: number
|
||||
licenses?: number
|
||||
personnel?: number
|
||||
}
|
||||
currency?: string
|
||||
lastUpdated?: string
|
||||
}
|
||||
|
||||
export interface InfrastructureSummary {
|
||||
totalCountries: number
|
||||
totalRegions: number
|
||||
totalCost: number
|
||||
deploymentProgress: {
|
||||
planned: number
|
||||
inProgress: number
|
||||
complete: number
|
||||
blocked: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Utility function to merge Tailwind CSS classes
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
countrySchema,
|
||||
topologyNodeSchema,
|
||||
topologyEdgeSchema,
|
||||
networkTopologySchema,
|
||||
complianceRequirementSchema,
|
||||
deploymentMilestoneSchema,
|
||||
costEstimateSchema,
|
||||
} from '../schemas/infrastructure'
|
||||
|
||||
describe('Infrastructure Validation Schemas', () => {
|
||||
describe('countrySchema', () => {
|
||||
it('should validate a valid country', () => {
|
||||
const validCountry = {
|
||||
name: 'Italy',
|
||||
region: 'Europe',
|
||||
relationshipType: 'Full Diplomatic Relations',
|
||||
priority: 'High',
|
||||
cloudflareCoverage: true,
|
||||
networkInfrastructurePriority: 'Critical',
|
||||
}
|
||||
|
||||
expect(() => countrySchema.parse(validCountry)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid region', () => {
|
||||
const invalidCountry = {
|
||||
name: 'Italy',
|
||||
region: 'Invalid Region',
|
||||
relationshipType: 'Full Diplomatic Relations',
|
||||
priority: 'High',
|
||||
cloudflareCoverage: true,
|
||||
networkInfrastructurePriority: 'Critical',
|
||||
}
|
||||
|
||||
expect(() => countrySchema.parse(invalidCountry)).toThrow()
|
||||
})
|
||||
|
||||
it('should reject missing required fields', () => {
|
||||
const invalidCountry = {
|
||||
name: 'Italy',
|
||||
// Missing required fields
|
||||
}
|
||||
|
||||
expect(() => countrySchema.parse(invalidCountry)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('topologyNodeSchema', () => {
|
||||
it('should validate a valid node', () => {
|
||||
const validNode = {
|
||||
id: 'node-1',
|
||||
type: 'vm',
|
||||
label: 'VM 1',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
position: { x: 100, y: 200 },
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
expect(() => topologyNodeSchema.parse(validNode)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid node type', () => {
|
||||
const invalidNode = {
|
||||
id: 'node-1',
|
||||
type: 'invalid-type',
|
||||
label: 'Node',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
expect(() => topologyNodeSchema.parse(invalidNode)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('topologyEdgeSchema', () => {
|
||||
it('should validate a valid edge', () => {
|
||||
const validEdge = {
|
||||
id: 'edge-1',
|
||||
source: 'node-1',
|
||||
target: 'node-2',
|
||||
type: 'network-route',
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
expect(() => topologyEdgeSchema.parse(validEdge)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('networkTopologySchema', () => {
|
||||
it('should validate a valid topology', () => {
|
||||
const validTopology = {
|
||||
id: 'topology-1',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
nodes: [
|
||||
{
|
||||
id: 'node-1',
|
||||
type: 'vm',
|
||||
label: 'VM 1',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'node-1',
|
||||
target: 'node-2',
|
||||
type: 'network-route',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
expect(() => networkTopologySchema.parse(validTopology)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('complianceRequirementSchema', () => {
|
||||
it('should validate a valid compliance requirement', () => {
|
||||
const validRequirement = {
|
||||
country: 'Italy',
|
||||
region: 'Europe',
|
||||
frameworks: ['GDPR'],
|
||||
status: 'Compliant',
|
||||
requirements: ['Data protection'],
|
||||
}
|
||||
|
||||
expect(() => complianceRequirementSchema.parse(validRequirement)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid status', () => {
|
||||
const invalidRequirement = {
|
||||
country: 'Italy',
|
||||
region: 'Europe',
|
||||
frameworks: ['GDPR'],
|
||||
status: 'Invalid Status',
|
||||
requirements: [],
|
||||
}
|
||||
|
||||
expect(() => complianceRequirementSchema.parse(invalidRequirement)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deploymentMilestoneSchema', () => {
|
||||
it('should validate a valid milestone', () => {
|
||||
const validMilestone = {
|
||||
id: 'milestone-1',
|
||||
title: 'Deploy Infrastructure',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
priority: 'High',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-31',
|
||||
status: 'Planned',
|
||||
dependencies: [],
|
||||
cost: 10000,
|
||||
description: 'Deploy infrastructure',
|
||||
}
|
||||
|
||||
expect(() => deploymentMilestoneSchema.parse(validMilestone)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid priority', () => {
|
||||
const invalidMilestone = {
|
||||
id: 'milestone-1',
|
||||
title: 'Deploy',
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
priority: 'Invalid Priority',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-31',
|
||||
status: 'Planned',
|
||||
dependencies: [],
|
||||
}
|
||||
|
||||
expect(() => deploymentMilestoneSchema.parse(invalidMilestone)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('costEstimateSchema', () => {
|
||||
it('should validate a valid cost estimate', () => {
|
||||
const validEstimate = {
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
category: 'Infrastructure',
|
||||
monthly: 1000,
|
||||
annual: 12000,
|
||||
breakdown: {
|
||||
compute: 500,
|
||||
storage: 200,
|
||||
network: 300,
|
||||
},
|
||||
currency: 'USD',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
expect(() => costEstimateSchema.parse(validEstimate)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject negative costs', () => {
|
||||
const invalidEstimate = {
|
||||
region: 'Europe',
|
||||
entity: 'SMOM',
|
||||
category: 'Infrastructure',
|
||||
monthly: -1000,
|
||||
annual: -12000,
|
||||
breakdown: {},
|
||||
currency: 'USD',
|
||||
}
|
||||
|
||||
expect(() => costEstimateSchema.parse(invalidEstimate)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Zod validation schemas for infrastructure documentation types
|
||||
*/
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
// Region enum
|
||||
export const regionSchema = z.enum([
|
||||
'Africa (Sub-Saharan)',
|
||||
'Middle East & North Africa',
|
||||
'Americas',
|
||||
'Asia-Pacific',
|
||||
'Europe',
|
||||
])
|
||||
|
||||
// Relationship type enum
|
||||
export const relationshipTypeSchema = z.enum([
|
||||
'Full Diplomatic Relations',
|
||||
'Official (Non-Diplomatic)',
|
||||
'Ambassador Level',
|
||||
'Full Diplomatic Relations (Special Mission)',
|
||||
])
|
||||
|
||||
// Priority enum
|
||||
export const prioritySchema = z.enum(['Critical', 'High', 'Medium', 'Low'])
|
||||
|
||||
// Compliance status enum
|
||||
export const complianceStatusSchema = z.enum([
|
||||
'Compliant',
|
||||
'Partial',
|
||||
'Pending',
|
||||
'Non-Compliant',
|
||||
])
|
||||
|
||||
// Deployment status enum
|
||||
export const deploymentStatusSchema = z.enum([
|
||||
'Planned',
|
||||
'In Progress',
|
||||
'Complete',
|
||||
'Blocked',
|
||||
])
|
||||
|
||||
// Cost category enum
|
||||
export const costCategorySchema = z.enum([
|
||||
'Infrastructure',
|
||||
'Network',
|
||||
'Compliance',
|
||||
'Operations',
|
||||
])
|
||||
|
||||
// Topology node type enum
|
||||
export const topologyNodeTypeSchema = z.enum([
|
||||
'region',
|
||||
'datacenter',
|
||||
'tunnel',
|
||||
'vm',
|
||||
'service',
|
||||
])
|
||||
|
||||
// Topology edge type enum
|
||||
export const topologyEdgeTypeSchema = z.enum(['tunnel', 'peering', 'network-route'])
|
||||
|
||||
// Coordinates schema
|
||||
export const coordinatesSchema = z
|
||||
.object({
|
||||
lat: z.number().min(-90).max(90),
|
||||
lng: z.number().min(-180).max(180),
|
||||
})
|
||||
.optional()
|
||||
|
||||
// Country schema
|
||||
export const countrySchema = z.object({
|
||||
name: z.string().min(1, 'Country name is required'),
|
||||
region: regionSchema,
|
||||
relationshipType: relationshipTypeSchema,
|
||||
priority: prioritySchema,
|
||||
cloudflareCoverage: z.boolean(),
|
||||
networkInfrastructurePriority: z.string().min(1),
|
||||
notes: z.string().optional(),
|
||||
coordinates: coordinatesSchema,
|
||||
})
|
||||
|
||||
// Topology node schema
|
||||
export const topologyNodeSchema = z.object({
|
||||
id: z.string().min(1, 'Node ID is required'),
|
||||
type: topologyNodeTypeSchema,
|
||||
label: z.string().min(1, 'Node label is required'),
|
||||
region: z.string().min(1, 'Region is required'),
|
||||
entity: z.string().min(1, 'Entity is required'),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
metadata: z.record(z.any()).default({}),
|
||||
})
|
||||
|
||||
// Topology edge schema
|
||||
export const topologyEdgeSchema = z.object({
|
||||
id: z.string().min(1, 'Edge ID is required'),
|
||||
source: z.string().min(1, 'Source node ID is required'),
|
||||
target: z.string().min(1, 'Target node ID is required'),
|
||||
type: topologyEdgeTypeSchema,
|
||||
metadata: z.record(z.any()).default({}),
|
||||
})
|
||||
|
||||
// Network topology schema
|
||||
export const networkTopologySchema = z.object({
|
||||
nodes: z.array(topologyNodeSchema).min(0),
|
||||
edges: z.array(topologyEdgeSchema).min(0),
|
||||
region: z.string().min(1, 'Region is required'),
|
||||
entity: z.string().min(1, 'Entity is required'),
|
||||
lastUpdated: z.string().datetime({ message: 'Invalid datetime format' }),
|
||||
})
|
||||
|
||||
// Compliance requirement schema
|
||||
export const complianceRequirementSchema = z.object({
|
||||
country: z.string().min(1, 'Country is required'),
|
||||
region: regionSchema,
|
||||
frameworks: z.array(z.string().min(1)).min(1, 'At least one framework is required'),
|
||||
status: complianceStatusSchema,
|
||||
requirements: z.array(z.string()).min(0),
|
||||
lastAuditDate: z.string().datetime().optional(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
// Deployment milestone schema with date validation
|
||||
export const deploymentMilestoneSchema = z
|
||||
.object({
|
||||
id: z.string().min(1, 'Milestone ID is required'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
region: z.string().min(1, 'Region is required'),
|
||||
entity: z.string().min(1, 'Entity is required'),
|
||||
priority: prioritySchema,
|
||||
startDate: z.string().datetime({ message: 'Invalid start date format' }),
|
||||
endDate: z.string().datetime({ message: 'Invalid end date format' }),
|
||||
status: deploymentStatusSchema,
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
cost: z.number().min(0, 'Cost must be non-negative').optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const start = new Date(data.startDate)
|
||||
const end = new Date(data.endDate)
|
||||
return end > start
|
||||
},
|
||||
{
|
||||
message: 'End date must be after start date',
|
||||
path: ['endDate'],
|
||||
}
|
||||
)
|
||||
|
||||
// Cost breakdown schema
|
||||
export const costBreakdownSchema = z.object({
|
||||
compute: z.number().min(0).optional(),
|
||||
storage: z.number().min(0).optional(),
|
||||
network: z.number().min(0).optional(),
|
||||
licenses: z.number().min(0).optional(),
|
||||
personnel: z.number().min(0).optional(),
|
||||
})
|
||||
|
||||
// Cost estimate schema with breakdown validation
|
||||
export const costEstimateSchema = z
|
||||
.object({
|
||||
region: z.string().min(1, 'Region is required'),
|
||||
entity: z.string().min(1, 'Entity is required'),
|
||||
category: costCategorySchema,
|
||||
monthly: z.number().min(0, 'Monthly cost must be non-negative'),
|
||||
annual: z.number().min(0, 'Annual cost must be non-negative'),
|
||||
breakdown: costBreakdownSchema,
|
||||
currency: z.string().optional(),
|
||||
lastUpdated: z.string().datetime().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const breakdownSum =
|
||||
(data.breakdown.compute || 0) +
|
||||
(data.breakdown.storage || 0) +
|
||||
(data.breakdown.network || 0) +
|
||||
(data.breakdown.licenses || 0) +
|
||||
(data.breakdown.personnel || 0)
|
||||
// Allow small floating point differences
|
||||
return Math.abs(breakdownSum - data.monthly) < 0.01
|
||||
},
|
||||
{
|
||||
message: 'Breakdown sum must equal monthly cost',
|
||||
path: ['breakdown'],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Annual should be approximately monthly * 12 (allow small differences)
|
||||
const expectedAnnual = data.monthly * 12
|
||||
return Math.abs(data.annual - expectedAnnual) < 0.01
|
||||
},
|
||||
{
|
||||
message: 'Annual cost should be approximately monthly * 12',
|
||||
path: ['annual'],
|
||||
}
|
||||
)
|
||||
|
||||
// Infrastructure summary schema
|
||||
export const infrastructureSummarySchema = z.object({
|
||||
totalCountries: z.number().int().min(0),
|
||||
totalRegions: z.number().int().min(0),
|
||||
totalCost: z.number().min(0),
|
||||
deploymentProgress: z.object({
|
||||
planned: z.number().int().min(0),
|
||||
inProgress: z.number().int().min(0),
|
||||
complete: z.number().int().min(0),
|
||||
blocked: z.number().int().min(0),
|
||||
}),
|
||||
})
|
||||
|
||||
// Input schemas for mutations
|
||||
|
||||
// Update topology input
|
||||
export const updateTopologyInputSchema = z.object({
|
||||
nodes: z.array(topologyNodeSchema).optional(),
|
||||
edges: z.array(topologyEdgeSchema).optional(),
|
||||
region: z.string().min(1).optional(),
|
||||
entity: z.string().min(1).optional(),
|
||||
lastUpdated: z.string().datetime().optional(),
|
||||
})
|
||||
|
||||
// Create milestone input
|
||||
export const createMilestoneInputSchema = deploymentMilestoneSchema.omit({ id: true })
|
||||
|
||||
// Update milestone input
|
||||
export const updateMilestoneInputSchema = deploymentMilestoneSchema.partial().required({ id: true })
|
||||
|
||||
// Update compliance input
|
||||
export const updateComplianceInputSchema = complianceRequirementSchema.partial().required({
|
||||
country: true,
|
||||
})
|
||||
|
||||
// Update cost estimate input
|
||||
export const updateCostEstimateInputSchema = costEstimateSchema
|
||||
.partial()
|
||||
.required({ region: true, entity: true, category: true })
|
||||
|
||||
// Type exports for TypeScript inference
|
||||
export type CountryInput = z.infer<typeof countrySchema>
|
||||
export type TopologyNodeInput = z.infer<typeof topologyNodeSchema>
|
||||
export type TopologyEdgeInput = z.infer<typeof topologyEdgeSchema>
|
||||
export type NetworkTopologyInput = z.infer<typeof networkTopologySchema>
|
||||
export type ComplianceRequirementInput = z.infer<typeof complianceRequirementSchema>
|
||||
export type DeploymentMilestoneInput = z.infer<typeof deploymentMilestoneSchema>
|
||||
export type CostEstimateInput = z.infer<typeof costEstimateSchema>
|
||||
export type InfrastructureSummaryInput = z.infer<typeof infrastructureSummarySchema>
|
||||
export type UpdateTopologyInput = z.infer<typeof updateTopologyInputSchema>
|
||||
export type CreateMilestoneInput = z.infer<typeof createMilestoneInputSchema>
|
||||
export type UpdateMilestoneInput = z.infer<typeof updateMilestoneInputSchema>
|
||||
export type UpdateComplianceInput = z.infer<typeof updateComplianceInputSchema>
|
||||
export type UpdateCostEstimateInput = z.infer<typeof updateCostEstimateInputSchema>
|
||||
|
||||
Reference in New Issue
Block a user