UI styling improvements: dashboard headers and navigation

- Restore blue PageHeader on Dashboard (/app-components)
- Update homepage (/) with subtle header design without blue bar
- Add uniform PageHeader styling to application edit page
- Fix Rapporten link on homepage to point to /reports overview
- Improve header descriptions spacing for better readability
This commit is contained in:
2026-01-21 03:24:56 +01:00
parent e276e77fbc
commit cdee0e8819
138 changed files with 24551 additions and 3352 deletions

View File

@@ -1,11 +1,10 @@
import { Router } from 'express';
import { OBJECT_TYPES, SCHEMA_GENERATED_AT, SCHEMA_OBJECT_TYPE_COUNT, SCHEMA_TOTAL_ATTRIBUTES } from '../generated/jira-schema.js';
import type { ObjectTypeDefinition, AttributeDefinition } from '../generated/jira-schema.js';
import { dataService } from '../services/dataService.js';
import { schemaCacheService } from '../services/schemaCacheService.js';
import { schemaSyncService } from '../services/SchemaSyncService.js';
import { schemaMappingService } from '../services/schemaMappingService.js';
import { logger } from '../services/logger.js';
import { jiraAssetsClient } from '../services/jiraAssetsClient.js';
import { requireAuth, requirePermission } from '../middleware/authorization.js';
import type { CMDBObjectTypeName } from '../generated/jira-types.js';
const router = Router();
@@ -13,125 +12,53 @@ const router = Router();
router.use(requireAuth);
router.use(requirePermission('search'));
// Extended types for API response
interface ObjectTypeWithLinks extends ObjectTypeDefinition {
incomingLinks: Array<{
fromType: string;
fromTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
outgoingLinks: Array<{
toType: string;
toTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
}
interface SchemaResponse {
metadata: {
generatedAt: string;
objectTypeCount: number;
totalAttributes: number;
};
objectTypes: Record<string, ObjectTypeWithLinks>;
cacheCounts?: Record<string, number>; // Cache counts by type name (from objectsByType)
jiraCounts?: Record<string, number>; // Actual counts from Jira Assets API
}
/**
* GET /api/schema
* Returns the complete Jira Assets schema with object types, attributes, and links
* Data is fetched from database (via cache service)
*/
router.get('/', async (req, res) => {
try {
// Build links between object types
const objectTypesWithLinks: Record<string, ObjectTypeWithLinks> = {};
// Get schema from cache (which fetches from database)
const schema = await schemaCacheService.getSchema();
// First pass: convert all object types
for (const [typeName, typeDef] of Object.entries(OBJECT_TYPES)) {
objectTypesWithLinks[typeName] = {
...typeDef,
incomingLinks: [],
outgoingLinks: [],
};
}
// Optionally fetch Jira counts for comparison (can be slow, so make it optional)
let jiraCounts: Record<string, number> | undefined;
const includeJiraCounts = req.query.includeJiraCounts === 'true';
// Second pass: build link relationships
for (const [typeName, typeDef] of Object.entries(OBJECT_TYPES)) {
for (const attr of typeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName) {
// Add outgoing link from this type
objectTypesWithLinks[typeName].outgoingLinks.push({
toType: attr.referenceTypeName,
toTypeName: OBJECT_TYPES[attr.referenceTypeName]?.name || attr.referenceTypeName,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
// Add incoming link to the referenced type
if (objectTypesWithLinks[attr.referenceTypeName]) {
objectTypesWithLinks[attr.referenceTypeName].incomingLinks.push({
fromType: typeName,
fromTypeName: typeDef.name,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
}
if (includeJiraCounts) {
const typeNames = Object.keys(schema.objectTypes);
logger.info(`Schema: Fetching object counts from Jira Assets for ${typeNames.length} object types...`);
jiraCounts = {};
// Fetch counts in parallel for better performance, using schema mappings
const countPromises = typeNames.map(async (typeName) => {
try {
// Get schema ID for this type
const schemaId = await schemaMappingService.getSchemaId(typeName);
const count = await jiraAssetsClient.getObjectCount(typeName, schemaId);
jiraCounts![typeName] = count;
return { typeName, count };
} catch (error) {
logger.warn(`Schema: Failed to get count for ${typeName}`, error);
// Use 0 as fallback if API call fails
jiraCounts![typeName] = 0;
return { typeName, count: 0 };
}
}
});
await Promise.all(countPromises);
logger.info(`Schema: Fetched counts for ${Object.keys(jiraCounts).length} object types from Jira Assets`);
}
// Get cache counts (objectsByType) if available
let cacheCounts: Record<string, number> | undefined;
try {
const cacheStatus = await dataService.getCacheStatus();
cacheCounts = cacheStatus.objectsByType;
} catch (err) {
logger.debug('Could not fetch cache counts for schema response', err);
// Continue without cache counts - not critical
}
// Fetch actual counts from Jira Assets for all object types
// This ensures the counts match exactly what's in Jira Assets
const jiraCounts: Record<string, number> = {};
const typeNames = Object.keys(OBJECT_TYPES) as CMDBObjectTypeName[];
logger.info(`Schema: Fetching object counts from Jira Assets for ${typeNames.length} object types...`);
// Fetch counts in parallel for better performance
const countPromises = typeNames.map(async (typeName) => {
try {
const count = await jiraAssetsClient.getObjectCount(typeName);
jiraCounts[typeName] = count;
return { typeName, count };
} catch (error) {
logger.warn(`Schema: Failed to get count for ${typeName}`, error);
// Use 0 as fallback if API call fails
jiraCounts[typeName] = 0;
return { typeName, count: 0 };
}
});
await Promise.all(countPromises);
logger.info(`Schema: Fetched counts for ${Object.keys(jiraCounts).length} object types from Jira Assets`);
const response: SchemaResponse = {
metadata: {
generatedAt: SCHEMA_GENERATED_AT,
objectTypeCount: SCHEMA_OBJECT_TYPE_COUNT,
totalAttributes: SCHEMA_TOTAL_ATTRIBUTES,
},
objectTypes: objectTypesWithLinks,
cacheCounts,
const response = {
...schema,
jiraCounts,
};
res.json(response);
} catch (error) {
console.error('Failed to get schema:', error);
logger.error('Failed to get schema:', error);
res.status(500).json({ error: 'Failed to get schema' });
}
});
@@ -140,60 +67,62 @@ router.get('/', async (req, res) => {
* GET /api/schema/object-type/:typeName
* Returns details for a specific object type
*/
router.get('/object-type/:typeName', (req, res) => {
const { typeName } = req.params;
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
return res.status(404).json({ error: `Object type '${typeName}' not found` });
}
// Build links for this specific type
const incomingLinks: Array<{
fromType: string;
fromTypeName: string;
attributeName: string;
isMultiple: boolean;
}> = [];
const outgoingLinks: Array<{
toType: string;
toTypeName: string;
attributeName: string;
isMultiple: boolean;
}> = [];
// Outgoing links from this type
for (const attr of typeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName) {
outgoingLinks.push({
toType: attr.referenceTypeName,
toTypeName: OBJECT_TYPES[attr.referenceTypeName]?.name || attr.referenceTypeName,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
router.get('/object-type/:typeName', async (req, res) => {
try {
const { typeName } = req.params;
// Get schema from cache
const schema = await schemaCacheService.getSchema();
const typeDef = schema.objectTypes[typeName];
if (!typeDef) {
return res.status(404).json({ error: `Object type '${typeName}' not found` });
}
res.json(typeDef);
} catch (error) {
logger.error('Failed to get object type:', error);
res.status(500).json({ error: 'Failed to get object type' });
}
// Incoming links from other types
for (const [otherTypeName, otherTypeDef] of Object.entries(OBJECT_TYPES)) {
for (const attr of otherTypeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName === typeName) {
incomingLinks.push({
fromType: otherTypeName,
fromTypeName: otherTypeDef.name,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
}
}
});
/**
* POST /api/schema/discover
* Manually trigger schema synchronization from Jira API
* Requires manage_settings permission
*/
router.post('/discover', requirePermission('manage_settings'), async (req, res) => {
try {
logger.info('Schema: Manual schema sync triggered');
const result = await schemaSyncService.syncAll();
schemaCacheService.invalidate(); // Invalidate cache
res.json({
success: result.success,
message: 'Schema synchronization completed',
...result,
});
} catch (error) {
logger.error('Failed to sync schema:', error);
res.status(500).json({
error: 'Failed to sync schema',
details: error instanceof Error ? error.message : String(error),
});
}
});
/**
* GET /api/schema/sync-progress
* Get current sync progress
*/
router.get('/sync-progress', requirePermission('manage_settings'), async (req, res) => {
try {
const progress = schemaSyncService.getProgress();
res.json(progress);
} catch (error) {
logger.error('Failed to get sync progress:', error);
res.status(500).json({ error: 'Failed to get sync progress' });
}
res.json({
...typeDef,
incomingLinks,
outgoingLinks,
});
});
export default router;