@pixelated-tech/components 3.5.14 → 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 +4 -10
- 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 +8 -14
- 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 +10 -33
- package/dist/components/admin/site-health/site-health-overview.js +4 -10
- package/dist/components/admin/site-health/site-health-performance.js +4 -10
- package/dist/components/admin/site-health/site-health-security.js +6 -10
- package/dist/components/admin/site-health/site-health-seo.js +4 -10
- 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.css +7 -0
- package/dist/index.adminclient.js +2 -6
- package/dist/index.adminserver.js +6 -5
- 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/index.adminclient.d.ts +2 -6
- package/dist/types/index.adminserver.d.ts +6 -5
- 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,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 { formatAuditItem, getAuditScoreIcon, getScoreColor } from './site-health-utils';
|
|
@@ -8,15 +7,10 @@ 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) => {
|
|
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) => {
|
|
20
14
|
if (!data?.data || data.data.length === 0) {
|
|
21
15
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No accessibility data available for this site." }));
|
|
22
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
|
}
|
|
@@ -3,110 +3,4 @@
|
|
|
3
3
|
* Server-side utilities for Google Analytics data retrieval
|
|
4
4
|
*/
|
|
5
5
|
"use server";
|
|
6
|
-
|
|
7
|
-
import { createAnalyticsClient } from './google-api-auth';
|
|
8
|
-
// Cache for analytics data (1 hour)
|
|
9
|
-
const analyticsCache = new RouteCache();
|
|
10
|
-
/**
|
|
11
|
-
* Get Google Analytics data for a site with current/previous period comparison
|
|
12
|
-
*/
|
|
13
|
-
export async function getGoogleAnalyticsData(config, siteName, startDate, endDate) {
|
|
14
|
-
try {
|
|
15
|
-
// Check cache first
|
|
16
|
-
const cacheKey = `analytics-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
17
|
-
const cached = analyticsCache.get(cacheKey);
|
|
18
|
-
if (cached) {
|
|
19
|
-
return { success: true, data: cached };
|
|
20
|
-
}
|
|
21
|
-
if (!config.ga4PropertyId || config.ga4PropertyId === 'GA4_PROPERTY_ID_HERE') {
|
|
22
|
-
return {
|
|
23
|
-
success: false,
|
|
24
|
-
error: 'GA4 Property ID not configured for this site'
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Set up authentication
|
|
28
|
-
const authResult = await createAnalyticsClient(config);
|
|
29
|
-
if (!authResult.success) {
|
|
30
|
-
return {
|
|
31
|
-
success: false,
|
|
32
|
-
error: authResult.error || 'Authentication failed'
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const analyticsData = authResult.client;
|
|
36
|
-
// Calculate date ranges
|
|
37
|
-
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
38
|
-
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
39
|
-
// Calculate previous period (same duration before the current period)
|
|
40
|
-
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
41
|
-
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
42
|
-
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
43
|
-
const currentStartStr = currentStartDate.toISOString().split('T')[0];
|
|
44
|
-
const currentEndStr = currentEndDate.toISOString().split('T')[0];
|
|
45
|
-
const previousStartStr = previousStartDate.toISOString().split('T')[0];
|
|
46
|
-
const previousEndStr = previousEndDate.toISOString().split('T')[0];
|
|
47
|
-
// Fetch current period data
|
|
48
|
-
const currentResponse = await analyticsData.properties.runReport({
|
|
49
|
-
property: `properties/${config.ga4PropertyId}`,
|
|
50
|
-
requestBody: {
|
|
51
|
-
dateRanges: [{ startDate: currentStartStr, endDate: currentEndStr }],
|
|
52
|
-
dimensions: [{ name: 'date' }],
|
|
53
|
-
metrics: [{ name: 'screenPageViews' }],
|
|
54
|
-
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
// Fetch previous period data
|
|
58
|
-
const previousResponse = await analyticsData.properties.runReport({
|
|
59
|
-
property: `properties/${config.ga4PropertyId}`,
|
|
60
|
-
requestBody: {
|
|
61
|
-
dateRanges: [{ startDate: previousStartStr, endDate: previousEndStr }],
|
|
62
|
-
dimensions: [{ name: 'date' }],
|
|
63
|
-
metrics: [{ name: 'screenPageViews' }],
|
|
64
|
-
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
// Create a map of previous period data by date
|
|
68
|
-
const previousDataMap = new Map();
|
|
69
|
-
previousResponse.data.rows?.forEach((row) => {
|
|
70
|
-
const dateStr = row.dimensionValues?.[0]?.value || '';
|
|
71
|
-
if (dateStr) {
|
|
72
|
-
previousDataMap.set(dateStr, parseInt(row.metricValues?.[0]?.value || '0'));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
// Combine current and previous period data
|
|
76
|
-
const chartData = [];
|
|
77
|
-
const daysInRange = Math.ceil((currentEndDate.getTime() - currentStartDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
78
|
-
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
79
|
-
const currentDate = new Date(currentEndDate);
|
|
80
|
-
currentDate.setDate(currentDate.getDate() - i);
|
|
81
|
-
const currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
82
|
-
// Calculate corresponding previous period date
|
|
83
|
-
const previousDate = new Date(currentDate.getTime() - periodDuration);
|
|
84
|
-
const previousDateStr = previousDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
85
|
-
// Get current period data
|
|
86
|
-
const currentRow = currentResponse.data.rows?.find((row) => row.dimensionValues?.[0]?.value === currentDateStr);
|
|
87
|
-
const currentPageViews = parseInt(currentRow?.metricValues?.[0]?.value || '0');
|
|
88
|
-
// Get previous period data
|
|
89
|
-
const previousPageViews = previousDataMap.get(previousDateStr) || 0;
|
|
90
|
-
// Format date for display
|
|
91
|
-
const formattedDate = currentDate.toLocaleDateString('en-US', {
|
|
92
|
-
month: 'short',
|
|
93
|
-
day: 'numeric'
|
|
94
|
-
});
|
|
95
|
-
chartData.push({
|
|
96
|
-
date: formattedDate,
|
|
97
|
-
currentPageViews: currentPageViews,
|
|
98
|
-
previousPageViews: previousPageViews,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// Cache the result
|
|
102
|
-
analyticsCache.set(cacheKey, chartData);
|
|
103
|
-
return { success: true, data: chartData };
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
console.error('Google Analytics error:', error);
|
|
107
|
-
return {
|
|
108
|
-
success: false,
|
|
109
|
-
error: error.message
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
6
|
+
export {};
|