@pixelated-tech/components 3.5.13 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/admin/site-health/google.api.integration.js +258 -0
- package/dist/components/admin/site-health/google.api.utils.js +47 -0
- package/dist/components/admin/site-health/site-health-accessibility.js +5 -103
- package/dist/components/admin/site-health/site-health-axe-core.js +4 -10
- package/dist/components/admin/site-health/site-health-cloudwatch.js +24 -26
- package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +4 -10
- package/dist/components/admin/site-health/site-health-github.js +9 -15
- package/dist/components/admin/site-health/site-health-google-analytics.integration.js +1 -107
- package/dist/components/admin/site-health/site-health-google-analytics.js +21 -29
- package/dist/components/admin/site-health/site-health-google-search-console.integration.js +1 -113
- package/dist/components/admin/site-health/site-health-google-search-console.js +22 -28
- package/dist/components/admin/site-health/site-health-on-site-seo.js +8 -82
- package/dist/components/admin/site-health/site-health-overview.js +5 -19
- package/dist/components/admin/site-health/site-health-performance.js +5 -167
- package/dist/components/admin/site-health/site-health-security.js +7 -148
- package/dist/components/admin/site-health/site-health-seo.js +5 -103
- package/dist/components/admin/site-health/site-health-template.js +68 -43
- package/dist/components/admin/site-health/site-health-uptime.js +4 -9
- package/dist/components/admin/site-health/site-health-utils.js +170 -0
- package/dist/components/admin/site-health/site-health.css +8 -9
- package/dist/index.adminclient.js +2 -5
- package/dist/index.adminserver.js +6 -4
- package/dist/index.js +39 -45
- package/dist/types/components/admin/site-health/google.api.integration.d.ts +82 -0
- package/dist/types/components/admin/site-health/google.api.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/google.api.utils.d.ts +32 -0
- package/dist/types/components/admin/site-health/google.api.utils.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-cloudwatch.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +1 -21
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +1 -41
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts +8 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-utils.d.ts +17 -0
- package/dist/types/components/admin/site-health/site-health-utils.d.ts.map +1 -0
- package/dist/types/index.adminclient.d.ts +2 -5
- package/dist/types/index.adminserver.d.ts +6 -4
- package/dist/types/index.d.ts +38 -44
- package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -1
- package/dist/types/tests/google.api.integration.test.d.ts +2 -0
- package/dist/types/tests/google.api.integration.test.d.ts.map +1 -0
- package/dist/types/tests/google.api.utils.test.d.ts +5 -0
- package/dist/types/tests/google.api.utils.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/components/admin/site-health/google-api-auth.js +0 -69
- package/dist/types/components/admin/site-health/google-api-auth.d.ts +0 -37
- package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -271,6 +271,8 @@ import { Accordion, Callout } from '@pixelated-tech/components';
|
|
|
271
271
|
|
|
272
272
|
For detailed usage examples and API documentation, see the [Component Reference Guide](docs/components.md).
|
|
273
273
|
|
|
274
|
+
For administrative components and site management features, see the [Admin Components Guide](docs/admin.md).
|
|
275
|
+
|
|
274
276
|
### Storybook Interactive Demos
|
|
275
277
|
|
|
276
278
|
Explore all components with live, interactive examples:
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google API Integration Services
|
|
3
|
+
* Centralized integration for Google services (Analytics, Search Console, etc.)
|
|
4
|
+
* Combines authentication, caching, and data processing logic
|
|
5
|
+
*/
|
|
6
|
+
"use server";
|
|
7
|
+
import { google } from 'googleapis';
|
|
8
|
+
import { RouteCache } from './site-health-cache';
|
|
9
|
+
import { calculateDateRanges, formatChartDate, getCachedData, setCachedData } from './google.api.utils';
|
|
10
|
+
/**
|
|
11
|
+
* Create authenticated Google API client for a specific service
|
|
12
|
+
*/
|
|
13
|
+
export async function createGoogleAuthClient(config, scopes) {
|
|
14
|
+
try {
|
|
15
|
+
let auth;
|
|
16
|
+
if (config.serviceAccountKey) {
|
|
17
|
+
// Use service account authentication (recommended)
|
|
18
|
+
const credentials = JSON.parse(config.serviceAccountKey);
|
|
19
|
+
auth = new google.auth.GoogleAuth({
|
|
20
|
+
credentials,
|
|
21
|
+
scopes,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else if (config.clientId && config.clientSecret && config.refreshToken) {
|
|
25
|
+
// Fallback to OAuth2 (deprecated for server-side apps)
|
|
26
|
+
const oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret);
|
|
27
|
+
oauth2Client.setCredentials({
|
|
28
|
+
refresh_token: config.refreshToken,
|
|
29
|
+
});
|
|
30
|
+
auth = oauth2Client;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: 'Google credentials not configured. Set GOOGLE_SERVICE_ACCOUNT_KEY or OAuth credentials.'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { success: true, auth };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: `Authentication failed: ${error.message}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create Analytics Data API client
|
|
49
|
+
*/
|
|
50
|
+
export async function createAnalyticsClient(config) {
|
|
51
|
+
const result = await createGoogleAuthClient(config, ['https://www.googleapis.com/auth/analytics.readonly']);
|
|
52
|
+
if (!result.success)
|
|
53
|
+
return result;
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
client: google.analyticsdata({ version: 'v1beta', auth: result.auth }),
|
|
57
|
+
auth: result.auth
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create Search Console API client
|
|
62
|
+
*/
|
|
63
|
+
export async function createSearchConsoleClient(config) {
|
|
64
|
+
const result = await createGoogleAuthClient(config, ['https://www.googleapis.com/auth/webmasters.readonly']);
|
|
65
|
+
if (!result.success)
|
|
66
|
+
return result;
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
client: google.searchconsole({ version: 'v1', auth: result.auth }),
|
|
70
|
+
auth: result.auth
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Cache for analytics data (1 hour)
|
|
74
|
+
const analyticsCache = new RouteCache();
|
|
75
|
+
/**
|
|
76
|
+
* Get Google Analytics data for a site with current/previous period comparison
|
|
77
|
+
*/
|
|
78
|
+
export async function getGoogleAnalyticsData(config, siteName, startDate, endDate) {
|
|
79
|
+
try {
|
|
80
|
+
// Check cache first
|
|
81
|
+
const cacheKey = `analytics-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
82
|
+
const cached = getCachedData(analyticsCache, cacheKey);
|
|
83
|
+
if (cached) {
|
|
84
|
+
return { success: true, data: cached };
|
|
85
|
+
}
|
|
86
|
+
if (!config.ga4PropertyId || config.ga4PropertyId === 'GA4_PROPERTY_ID_HERE') {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: 'GA4 Property ID not configured for this site'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Set up authentication
|
|
93
|
+
const authResult = await createAnalyticsClient(config);
|
|
94
|
+
if (!authResult.success) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: authResult.error || 'Authentication failed'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const analyticsData = authResult.client;
|
|
101
|
+
const dateRange = calculateDateRanges(startDate, endDate);
|
|
102
|
+
// Fetch current period data
|
|
103
|
+
const currentResponse = await analyticsData.properties.runReport({
|
|
104
|
+
property: `properties/${config.ga4PropertyId}`,
|
|
105
|
+
requestBody: {
|
|
106
|
+
dateRanges: [{ startDate: dateRange.currentStartStr, endDate: dateRange.currentEndStr }],
|
|
107
|
+
dimensions: [{ name: 'date' }],
|
|
108
|
+
metrics: [{ name: 'screenPageViews' }],
|
|
109
|
+
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
// Fetch previous period data
|
|
113
|
+
const previousResponse = await analyticsData.properties.runReport({
|
|
114
|
+
property: `properties/${config.ga4PropertyId}`,
|
|
115
|
+
requestBody: {
|
|
116
|
+
dateRanges: [{ startDate: dateRange.previousStartStr, endDate: dateRange.previousEndStr }],
|
|
117
|
+
dimensions: [{ name: 'date' }],
|
|
118
|
+
metrics: [{ name: 'screenPageViews' }],
|
|
119
|
+
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
// Create a map of previous period data by date
|
|
123
|
+
const previousDataMap = new Map();
|
|
124
|
+
previousResponse.data.rows?.forEach((row) => {
|
|
125
|
+
const dateStr = row.dimensionValues?.[0]?.value || '';
|
|
126
|
+
if (dateStr) {
|
|
127
|
+
previousDataMap.set(dateStr, parseInt(row.metricValues?.[0]?.value || '0'));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Combine current and previous period data
|
|
131
|
+
const chartData = [];
|
|
132
|
+
const daysInRange = Math.ceil((dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()) / (24 * 60 * 60 * 1000));
|
|
133
|
+
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
134
|
+
const currentDate = new Date(dateRange.currentEnd);
|
|
135
|
+
currentDate.setDate(currentDate.getDate() - i);
|
|
136
|
+
const currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
137
|
+
// Calculate corresponding previous period date
|
|
138
|
+
const previousDate = new Date(currentDate.getTime() - (dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()));
|
|
139
|
+
const previousDateStr = previousDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
140
|
+
// Get current period data
|
|
141
|
+
const currentRow = currentResponse.data.rows?.find((row) => row.dimensionValues?.[0]?.value === currentDateStr);
|
|
142
|
+
const currentPageViews = parseInt(currentRow?.metricValues?.[0]?.value || '0');
|
|
143
|
+
// Get previous period data
|
|
144
|
+
const previousPageViews = previousDataMap.get(previousDateStr) || 0;
|
|
145
|
+
chartData.push({
|
|
146
|
+
date: formatChartDate(currentDate),
|
|
147
|
+
currentPageViews: currentPageViews,
|
|
148
|
+
previousPageViews: previousPageViews,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Cache the result
|
|
152
|
+
setCachedData(analyticsCache, cacheKey, chartData);
|
|
153
|
+
return { success: true, data: chartData };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('Google Analytics error:', error);
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: error.message
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Cache for search console data (1 hour)
|
|
164
|
+
const searchConsoleCache = new RouteCache();
|
|
165
|
+
/**
|
|
166
|
+
* Get Google Search Console data for a site with current/previous period comparison
|
|
167
|
+
*/
|
|
168
|
+
export async function getSearchConsoleData(config, siteName, startDate, endDate) {
|
|
169
|
+
try {
|
|
170
|
+
// Check cache first
|
|
171
|
+
const cacheKey = `searchconsole-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
172
|
+
const cached = getCachedData(searchConsoleCache, cacheKey);
|
|
173
|
+
if (cached) {
|
|
174
|
+
return { success: true, data: cached };
|
|
175
|
+
}
|
|
176
|
+
if (!config.siteUrl) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: 'Site URL not configured for Search Console'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Set up authentication
|
|
183
|
+
const authResult = await createSearchConsoleClient(config);
|
|
184
|
+
if (!authResult.success) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: authResult.error || 'Authentication failed'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const searchconsole = authResult.client;
|
|
191
|
+
const dateRange = calculateDateRanges(startDate, endDate);
|
|
192
|
+
// Fetch current period data
|
|
193
|
+
const currentResponse = await searchconsole.searchanalytics.query({
|
|
194
|
+
siteUrl: config.siteUrl,
|
|
195
|
+
requestBody: {
|
|
196
|
+
startDate: dateRange.currentStartStr,
|
|
197
|
+
endDate: dateRange.currentEndStr,
|
|
198
|
+
dimensions: ['date'],
|
|
199
|
+
rowLimit: 10000,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
// Fetch previous period data
|
|
203
|
+
const previousResponse = await searchconsole.searchanalytics.query({
|
|
204
|
+
siteUrl: config.siteUrl,
|
|
205
|
+
requestBody: {
|
|
206
|
+
startDate: dateRange.previousStartStr,
|
|
207
|
+
endDate: dateRange.previousEndStr,
|
|
208
|
+
dimensions: ['date'],
|
|
209
|
+
rowLimit: 10000,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
// Create a map of previous period data by date
|
|
213
|
+
const previousDataMap = new Map();
|
|
214
|
+
previousResponse.data.rows?.forEach((row) => {
|
|
215
|
+
const dateStr = row.keys?.[0] || '';
|
|
216
|
+
if (dateStr) {
|
|
217
|
+
previousDataMap.set(dateStr, {
|
|
218
|
+
clicks: parseFloat(String(row.clicks || '0')),
|
|
219
|
+
impressions: parseFloat(String(row.impressions || '0'))
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// Combine current and previous period data
|
|
224
|
+
const chartData = [];
|
|
225
|
+
const daysInRange = Math.ceil((dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()) / (24 * 60 * 60 * 1000));
|
|
226
|
+
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
227
|
+
const currentDate = new Date(dateRange.currentEnd);
|
|
228
|
+
currentDate.setDate(currentDate.getDate() - i);
|
|
229
|
+
const currentDateStr = currentDate.toISOString().split('T')[0];
|
|
230
|
+
// Calculate corresponding previous period date
|
|
231
|
+
const previousDate = new Date(currentDate.getTime() - (dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()));
|
|
232
|
+
const previousDateStr = previousDate.toISOString().split('T')[0];
|
|
233
|
+
// Get current period data
|
|
234
|
+
const currentRow = currentResponse.data.rows?.find((row) => row.keys?.[0] === currentDateStr);
|
|
235
|
+
const currentClicks = parseFloat(String(currentRow?.clicks || '0'));
|
|
236
|
+
const currentImpressions = parseFloat(String(currentRow?.impressions || '0'));
|
|
237
|
+
// Get previous period data
|
|
238
|
+
const previousData = previousDataMap.get(previousDateStr) || { clicks: 0, impressions: 0 };
|
|
239
|
+
chartData.push({
|
|
240
|
+
date: formatChartDate(currentDate),
|
|
241
|
+
currentImpressions: Math.round(currentImpressions),
|
|
242
|
+
currentClicks: Math.round(currentClicks),
|
|
243
|
+
previousImpressions: Math.round(previousData.impressions),
|
|
244
|
+
previousClicks: Math.round(previousData.clicks),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Cache the result
|
|
248
|
+
setCachedData(searchConsoleCache, cacheKey, chartData);
|
|
249
|
+
return { success: true, data: chartData };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error('Google Search Console error:', error);
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: error.message
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google API Integration Utilities
|
|
3
|
+
* Shared utility functions for Google API integrations
|
|
4
|
+
* These are NOT server actions - just regular utility functions
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Calculate date ranges for current and previous periods
|
|
8
|
+
*/
|
|
9
|
+
export function calculateDateRanges(startDate, endDate) {
|
|
10
|
+
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
11
|
+
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
12
|
+
// Calculate previous period (same duration before the current period)
|
|
13
|
+
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
14
|
+
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
15
|
+
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
16
|
+
return {
|
|
17
|
+
currentStart: currentStartDate,
|
|
18
|
+
currentEnd: currentEndDate,
|
|
19
|
+
previousStart: previousStartDate,
|
|
20
|
+
previousEnd: previousEndDate,
|
|
21
|
+
currentStartStr: currentStartDate.toISOString().split('T')[0],
|
|
22
|
+
currentEndStr: currentEndDate.toISOString().split('T')[0],
|
|
23
|
+
previousStartStr: previousStartDate.toISOString().split('T')[0],
|
|
24
|
+
previousEndStr: previousEndDate.toISOString().split('T')[0],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format date for chart display
|
|
29
|
+
*/
|
|
30
|
+
export function formatChartDate(date) {
|
|
31
|
+
return date.toLocaleDateString('en-US', {
|
|
32
|
+
month: 'short',
|
|
33
|
+
day: 'numeric'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get cached data or null if not cached
|
|
38
|
+
*/
|
|
39
|
+
export function getCachedData(cache, cacheKey) {
|
|
40
|
+
return cache.get(cacheKey);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set cached data
|
|
44
|
+
*/
|
|
45
|
+
export function setCachedData(cache, cacheKey, data) {
|
|
46
|
+
cache.set(cacheKey, data);
|
|
47
|
+
}
|
|
@@ -1,114 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
3
|
import PropTypes from 'prop-types';
|
|
5
4
|
import { SiteHealthTemplate } from './site-health-template';
|
|
6
|
-
import {
|
|
5
|
+
import { formatAuditItem, getAuditScoreIcon, getScoreColor } from './site-health-utils';
|
|
7
6
|
SiteHealthAccessibility.propTypes = {
|
|
8
7
|
siteName: PropTypes.string.isRequired,
|
|
9
8
|
};
|
|
10
9
|
export function SiteHealthAccessibility({ siteName }) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error(result.error || 'Failed to fetch accessibility data');
|
|
16
|
-
}
|
|
17
|
-
return result;
|
|
18
|
-
}, []);
|
|
19
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Accessibility", fetchData: fetchAccessibilityData, children: (data) => {
|
|
20
|
-
const getScoreColor = (score) => {
|
|
21
|
-
return getScoreIndicator(score).color;
|
|
22
|
-
};
|
|
23
|
-
const getAuditScoreIcon = (score) => {
|
|
24
|
-
return getScoreIndicator(score).icon;
|
|
25
|
-
};
|
|
26
|
-
// Helper function to display audit item details
|
|
27
|
-
const formatAuditItem = (item, auditTitle) => {
|
|
28
|
-
// Handle URLs
|
|
29
|
-
if (item.url && typeof item.url === 'string') {
|
|
30
|
-
return item.url;
|
|
31
|
-
}
|
|
32
|
-
// Handle sources (like JavaScript files)
|
|
33
|
-
if (item.source && typeof item.source === 'string') {
|
|
34
|
-
return item.source;
|
|
35
|
-
}
|
|
36
|
-
// Handle text descriptions
|
|
37
|
-
if (item.text && typeof item.text === 'string') {
|
|
38
|
-
return item.text;
|
|
39
|
-
}
|
|
40
|
-
// Handle entities (like "Google Tag Manager")
|
|
41
|
-
if (item.entity && typeof item.entity === 'string') {
|
|
42
|
-
return item.entity;
|
|
43
|
-
}
|
|
44
|
-
// Handle nodes with selectors
|
|
45
|
-
if (item.node && typeof item.node === 'object' && 'selector' in item.node) {
|
|
46
|
-
return `Element: ${item.node.selector}`;
|
|
47
|
-
}
|
|
48
|
-
// Handle nodes with snippets
|
|
49
|
-
if (item.node && typeof item.node === 'object' && 'snippet' in item.node) {
|
|
50
|
-
const snippet = item.node.snippet;
|
|
51
|
-
return `Element: ${snippet.length > 50 ? snippet.substring(0, 50) + '...' : snippet}`;
|
|
52
|
-
}
|
|
53
|
-
// Handle origins (like domains)
|
|
54
|
-
if (item.origin && typeof item.origin === 'string') {
|
|
55
|
-
return item.origin;
|
|
56
|
-
}
|
|
57
|
-
// Handle labels
|
|
58
|
-
if (item.label && typeof item.label === 'string') {
|
|
59
|
-
return item.label;
|
|
60
|
-
}
|
|
61
|
-
// Handle numeric values with units
|
|
62
|
-
if (item.value && typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
|
|
63
|
-
const value = item.value;
|
|
64
|
-
return `${value.value}${item.unit || ''}`;
|
|
65
|
-
}
|
|
66
|
-
// Handle statistics
|
|
67
|
-
if (item.statistic && typeof item.statistic === 'string' && item.value) {
|
|
68
|
-
if (typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
|
|
69
|
-
const value = item.value;
|
|
70
|
-
return `${item.statistic}: ${value.value}`;
|
|
71
|
-
}
|
|
72
|
-
return item.statistic;
|
|
73
|
-
}
|
|
74
|
-
// Handle timing data with audit context
|
|
75
|
-
if (typeof item === 'number') {
|
|
76
|
-
let context = '';
|
|
77
|
-
if (auditTitle) {
|
|
78
|
-
if (auditTitle.toLowerCase().includes('server') || auditTitle.toLowerCase().includes('backend')) {
|
|
79
|
-
context = ' server response';
|
|
80
|
-
}
|
|
81
|
-
else if (auditTitle.toLowerCase().includes('network') || auditTitle.toLowerCase().includes('request')) {
|
|
82
|
-
context = ' network request';
|
|
83
|
-
}
|
|
84
|
-
else if (auditTitle.toLowerCase().includes('render') || auditTitle.toLowerCase().includes('blocking')) {
|
|
85
|
-
context = ' render blocking';
|
|
86
|
-
}
|
|
87
|
-
else if (auditTitle.toLowerCase().includes('javascript') || auditTitle.toLowerCase().includes('js')) {
|
|
88
|
-
context = ' JavaScript';
|
|
89
|
-
}
|
|
90
|
-
else if (auditTitle.toLowerCase().includes('image') || auditTitle.toLowerCase().includes('media')) {
|
|
91
|
-
context = ' media resource';
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return `${item.toFixed(2)}ms${context}`;
|
|
95
|
-
}
|
|
96
|
-
if (item.value && typeof item.value === 'number') {
|
|
97
|
-
const unit = item.unit || 'ms';
|
|
98
|
-
let context = '';
|
|
99
|
-
if (auditTitle && unit === 'ms') {
|
|
100
|
-
if (auditTitle.toLowerCase().includes('server')) {
|
|
101
|
-
context = ' server time';
|
|
102
|
-
}
|
|
103
|
-
else if (auditTitle.toLowerCase().includes('network')) {
|
|
104
|
-
context = ' network time';
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return `${item.value.toFixed(2)}${unit}${context}`;
|
|
108
|
-
}
|
|
109
|
-
// If we can't find anything meaningful, show a generic message
|
|
110
|
-
return 'Details available';
|
|
111
|
-
};
|
|
10
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Accessibility", endpoint: {
|
|
11
|
+
endpoint: '/api/site-health/core-web-vitals',
|
|
12
|
+
responseTransformer: (result) => result, // Result is already in the correct format
|
|
13
|
+
}, children: (data) => {
|
|
112
14
|
if (!data?.data || data.data.length === 0) {
|
|
113
15
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No accessibility data available for this site." }));
|
|
114
16
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
3
|
import PropTypes from 'prop-types';
|
|
5
4
|
import { SiteHealthTemplate } from './site-health-template';
|
|
6
5
|
import { getImpactIndicator, getIncompleteIndicator, getPassingIndicator } from './site-health-indicators';
|
|
@@ -8,14 +7,6 @@ SiteHealthAxeCore.propTypes = {
|
|
|
8
7
|
siteName: PropTypes.string.isRequired,
|
|
9
8
|
};
|
|
10
9
|
export function SiteHealthAxeCore({ siteName }) {
|
|
11
|
-
const fetchAxeCoreData = useCallback(async (site) => {
|
|
12
|
-
const response = await fetch(`/api/site-health/axe-core?siteName=${encodeURIComponent(site)}`);
|
|
13
|
-
const result = await response.json();
|
|
14
|
-
if (!result.success) {
|
|
15
|
-
throw new Error(result.error || 'Failed to fetch axe-core data');
|
|
16
|
-
}
|
|
17
|
-
return result;
|
|
18
|
-
}, []);
|
|
19
10
|
const getImpactColor = (impact) => {
|
|
20
11
|
return getImpactIndicator(impact).color;
|
|
21
12
|
};
|
|
@@ -34,7 +25,10 @@ export function SiteHealthAxeCore({ siteName }) {
|
|
|
34
25
|
}
|
|
35
26
|
return 'Unknown element';
|
|
36
27
|
};
|
|
37
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Axe-Core Accessibility",
|
|
28
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Axe-Core Accessibility", endpoint: {
|
|
29
|
+
endpoint: '/api/site-health/axe-core',
|
|
30
|
+
responseTransformer: (result) => result, // Result is already in the correct format
|
|
31
|
+
}, children: (data) => {
|
|
38
32
|
if (!data?.data || data.data.length === 0) {
|
|
39
33
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No axe-core data available for this site." }));
|
|
40
34
|
}
|
|
@@ -9,42 +9,40 @@ SiteHealthCloudwatch.propTypes = {
|
|
|
9
9
|
endDate: PropTypes.string,
|
|
10
10
|
};
|
|
11
11
|
export function SiteHealthCloudwatch({ siteName, startDate, endDate }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const result = await response.json();
|
|
23
|
-
if (!result.success) {
|
|
24
|
-
if (result.error?.includes('Health Check ID not configured')) {
|
|
25
|
-
throw new Error('Route53 Health Check ID not configured for this site');
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
throw new Error(result.error || 'Failed to load CloudWatch health check data');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return result.data;
|
|
32
|
-
};
|
|
33
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "CloudWatch Uptime", columnSpan: 2, fetchData: fetchCloudwatchData, children: (data) => {
|
|
34
|
-
if (!data || data.length === 0) {
|
|
12
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "CloudWatch Uptime", columnSpan: 2, endpoint: {
|
|
13
|
+
endpoint: '/api/site-health/cloudwatch',
|
|
14
|
+
params: {
|
|
15
|
+
...(startDate && { startDate }),
|
|
16
|
+
...(endDate && { endDate }),
|
|
17
|
+
},
|
|
18
|
+
responseTransformer: (result) => result.data, // Extract the data array from the response
|
|
19
|
+
}, children: (data) => {
|
|
20
|
+
// Ensure data is an array
|
|
21
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
35
22
|
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "No uptime data available. Route53 health checks may not be configured to send metrics to CloudWatch." }) }));
|
|
36
23
|
}
|
|
37
24
|
// Check if all data points have zero checks (no actual data)
|
|
38
|
-
const hasActualData = data.some((point) => point.totalChecks > 0);
|
|
25
|
+
const hasActualData = data.some((point) => point && typeof point === 'object' && point.totalChecks > 0);
|
|
39
26
|
if (!hasActualData) {
|
|
40
27
|
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsxs("div", { className: "health-text-secondary", children: ["Health check exists but has no metric data in CloudWatch for the selected period.", _jsx("br", {}), "Route53 health checks must be configured to send metrics to CloudWatch for historical data."] }) }));
|
|
41
28
|
}
|
|
42
|
-
|
|
29
|
+
// Filter out any invalid data points
|
|
30
|
+
const validData = data.filter((point) => point &&
|
|
31
|
+
typeof point === 'object' &&
|
|
32
|
+
typeof point.date === 'string' &&
|
|
33
|
+
typeof point.successCount === 'number' &&
|
|
34
|
+
typeof point.failureCount === 'number' &&
|
|
35
|
+
typeof point.totalChecks === 'number' &&
|
|
36
|
+
typeof point.successRate === 'number');
|
|
37
|
+
if (validData.length === 0) {
|
|
38
|
+
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "Invalid data format received from CloudWatch API." }) }));
|
|
39
|
+
}
|
|
40
|
+
return (_jsx("div", { children: _jsx("div", { style: { width: '100%', height: '400px', border: '1px solid #ddd' }, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: validData, margin: { top: 40, right: 30, left: 20, bottom: 5 }, children: [_jsx("text", { x: "50%", y: 20, textAnchor: "middle", fontSize: "16", fontWeight: "bold", fill: "#374151", children: "CloudWatch Health Check Availability Over Time" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 }, label: { value: 'Check Count', angle: -90, position: 'insideLeft' } }), _jsx(Tooltip, { formatter: (value, name) => [
|
|
43
41
|
value?.toLocaleString() || '0',
|
|
44
42
|
name || 'Unknown'
|
|
45
43
|
], labelFormatter: (label) => `Date: ${label}` }), _jsx(Legend, { wrapperStyle: {
|
|
46
44
|
fontSize: '12px',
|
|
47
45
|
paddingTop: '10px'
|
|
48
|
-
} }), _jsx(Bar, { dataKey: "successCount", stackId: "checks", fill: "#10b981", name: "Successful Checks", radius: [2, 2, 0, 0] }), _jsx(Bar, { dataKey: "failureCount", stackId: "checks", fill: "#ef4444", name: "Failed Checks", radius: [2, 2, 0, 0] })] }, `cloudwatch-chart-${
|
|
46
|
+
} }), _jsx(Bar, { dataKey: "successCount", stackId: "checks", fill: "#10b981", name: "Successful Checks", radius: [2, 2, 0, 0] }), _jsx(Bar, { dataKey: "failureCount", stackId: "checks", fill: "#ef4444", name: "Failed Checks", radius: [2, 2, 0, 0] })] }, `cloudwatch-chart-${validData.length}`) }) }) }));
|
|
49
47
|
} }));
|
|
50
48
|
}
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
3
|
import PropTypes from 'prop-types';
|
|
5
4
|
import { SiteHealthTemplate } from './site-health-template';
|
|
6
5
|
SiteHealthDependencyVulnerabilities.propTypes = {
|
|
7
6
|
siteName: PropTypes.string.isRequired,
|
|
8
7
|
};
|
|
9
8
|
export function SiteHealthDependencyVulnerabilities({ siteName }) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
throw new Error(result.error || 'Failed to fetch dependency data');
|
|
15
|
-
}
|
|
16
|
-
return result;
|
|
17
|
-
}, []);
|
|
18
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Dependency Vulnerability", fetchData: fetchDependencyData, children: (data) => {
|
|
9
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Dependency Vulnerability", endpoint: {
|
|
10
|
+
endpoint: '/api/site-health/security',
|
|
11
|
+
responseTransformer: (result) => result, // Result is already in the correct format
|
|
12
|
+
}, children: (data) => {
|
|
19
13
|
if (!data) {
|
|
20
14
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No dependency data available for this site." }));
|
|
21
15
|
}
|
|
@@ -9,20 +9,14 @@ SiteHealthGit.propTypes = {
|
|
|
9
9
|
endDate: PropTypes.string,
|
|
10
10
|
};
|
|
11
11
|
export function SiteHealthGit({ siteName, startDate, endDate }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw new Error('Failed to fetch git data');
|
|
21
|
-
}
|
|
22
|
-
const data = await response.json();
|
|
23
|
-
return data;
|
|
24
|
-
};
|
|
25
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Git Push Notes", fetchData: fetchGitData, children: (data) => {
|
|
12
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Git Push Notes", endpoint: {
|
|
13
|
+
endpoint: '/api/site-health/github',
|
|
14
|
+
params: {
|
|
15
|
+
...(startDate && { startDate }),
|
|
16
|
+
...(endDate && { endDate }),
|
|
17
|
+
},
|
|
18
|
+
responseTransformer: (result) => result, // Result is already in the correct format
|
|
19
|
+
}, children: (data) => {
|
|
26
20
|
if (!data || !data.success) {
|
|
27
21
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No git data available for this site." }));
|
|
28
22
|
}
|
|
@@ -32,7 +26,7 @@ export function SiteHealthGit({ siteName, startDate, endDate }) {
|
|
|
32
26
|
// Prepare table data
|
|
33
27
|
const tableData = (data.commits || []).map((commit) => ({
|
|
34
28
|
Date: new Date(commit.date).toLocaleDateString(),
|
|
35
|
-
Message: _jsx("span", {
|
|
29
|
+
Message: _jsx("span", { title: commit.message, children: commit.message }),
|
|
36
30
|
Version: commit.version ? (_jsx("span", { className: "health-version-tag", children: commit.version.split('~')[0] })) : (_jsx("span", { className: "health-text-muted", children: "-" }))
|
|
37
31
|
}));
|
|
38
32
|
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteName.replace('-', ' ') }), _jsx("div", { className: "health-section-list", children: tableData.length === 0 ? (_jsx("p", { className: "health-empty-state", children: "No recent commits found" })) : (_jsx(Table, { id: "git-table", data: tableData, altRowColor: "#DDD" })) }), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(data.timestamp).toLocaleString()] })] }));
|