diff --git a/plugins/Vercel/v1/configValidation.json b/plugins/Vercel/v1/configValidation.json new file mode 100644 index 00000000..2ac57f11 --- /dev/null +++ b/plugins/Vercel/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "currentUser" }, + "required": true, + "error": "Could not authenticate with Vercel. Check that your API Token is valid and has not expired.", + "success": "Connected to Vercel successfully." + } + ] +} diff --git a/plugins/Vercel/v1/custom_types.json b/plugins/Vercel/v1/custom_types.json new file mode 100644 index 00000000..5ad86d20 --- /dev/null +++ b/plugins/Vercel/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "Project", + "sourceType": "Project", + "icon": "rocket", + "singular": "Project", + "plural": "Projects" + }, + { + "name": "Domain", + "sourceType": "Domain", + "icon": "globe", + "singular": "Domain", + "plural": "Domains" + } +] diff --git a/plugins/Vercel/v1/dataStreams/activity.json b/plugins/Vercel/v1/dataStreams/activity.json new file mode 100644 index 00000000..8abd5b02 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/activity.json @@ -0,0 +1,36 @@ +{ + "name": "activity", + "displayName": "Activity", + "description": "Vercel account or team activity feed, one row per audit-style event. Backs activity and audit log tiles", + "tags": ["Activity"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v3/events", + "pathToData": "events", + "expandInnerObjects": true, + "getArgs": [ + { "key": "since", "value": "{{timeframe.start}}" }, + { "key": "until", "value": "{{timeframe.end}}" }, + { "key": "limit", "value": "100" } + ], + "paging": { "mode": "none" } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "role": "id", "visible": false }, + { "name": "type", "displayName": "Type" }, + { "name": "text", "displayName": "Summary", "role": "label" }, + { + "name": "actor", + "displayName": "Actor", + "computed": true, + "valueExpression": "{{ $['user.username'] || $['user.email'] || $['userId'] }}" + }, + { "name": "user.username", "displayName": "User", "visible": false }, + { "name": "user.email", "displayName": "Email", "visible": false }, + { "name": "userId", "displayName": "User ID", "visible": false }, + { "name": "createdAt", "displayName": "Created", "shape": "date", "role": "timestamp" } + ], + "timeframes": true +} diff --git a/plugins/Vercel/v1/dataStreams/cost.json b/plugins/Vercel/v1/dataStreams/cost.json new file mode 100644 index 00000000..82f237d1 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/cost.json @@ -0,0 +1,71 @@ +{ + "name": "cost", + "displayName": "Cost", + "description": "Vercel usage cost and consumption from the FOCUS billing endpoint, one row per daily charge", + "tags": ["Cost", "Billing"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/billing/charges", + "postRequestScript": "cost.js", + "getArgs": [ + { "key": "from", "value": "{{timeframe.start}}" }, + { "key": "to", "value": "{{timeframe.end}}" } + ], + "headers": [ + { + "key": "Accept-Encoding", + "value": "gzip" + } + ] + }, + "matches": "none", + "metadata": [ + { + "name": "service", + "displayName": "Service", + "shape": "string", + "role": "label" + }, + { + "name": "billedCost", + "displayName": "Billed Cost ($)", + "shape": [ + "currency", + { + "code": "usd", + "decimalPlaces": 2, + "thousandsSeparator": true + } + ], + "role": "value" + }, + { + "name": "effectiveCost", + "displayName": "Effective Cost ($)", + "shape": [ + "currency", + { + "code": "usd", + "decimalPlaces": 2, + "thousandsSeparator": true + } + ] + }, + { "name": "quantity", "displayName": "Quantity", "shape": "number" }, + { "name": "unit", "displayName": "Unit", "shape": "string" }, + { + "name": "projectName", + "displayName": "Project", + "shape": "string", + "role": "label" + }, + { + "name": "periodStart", + "displayName": "Period Start", + "shape": "date", + "role": "timestamp" + } + ], + "timeframes": ["last24hours", "last7days", "last30days", "thisMonth"] +} diff --git a/plugins/Vercel/v1/dataStreams/currentUser.json b/plugins/Vercel/v1/dataStreams/currentUser.json new file mode 100644 index 00000000..844ab4ac --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/currentUser.json @@ -0,0 +1,21 @@ +{ + "name": "currentUser", + "displayName": "Current User", + "description": "Returns the authenticated Vercel user. Used to validate the connection.", + "tags": [], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/user", + "postRequestScript": "currentUser.js" + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "visible": false }, + { "name": "username", "displayName": "Username", "role": "label" }, + { "name": "name", "displayName": "Name" }, + { "name": "email", "displayName": "Email" } + ], + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/Vercel/v1/dataStreams/deployments.json b/plugins/Vercel/v1/dataStreams/deployments.json new file mode 100644 index 00000000..42eea108 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/deployments.json @@ -0,0 +1,95 @@ +{ + "name": "deployments", + "displayName": "Deployments", + "description": "Vercel deployments across the account or a selected project, one row per deployment. Backs deployment health and history tiles", + "tags": ["Deployments"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v7/deployments", + "postRequestScript": "deployments.js", + "getArgs": [ + { "key": "since", "value": "{{timeframe.unixStart * 1000}}" } + ], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "until" } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "project", + "label": "Project (optional)", + "matches": { + "sourceType": { "type": "equals", "value": "Project" } + } + } + ], + "metadata": [ + { + "name": "uid", + "displayName": "ID", + "role": "value", + "visible": false + }, + { "name": "name", "displayName": "Name", "role": "label" }, + { + "name": "state", + "displayName": "State", + "shape": [ + "state", + { + "map": { + "success": ["READY"], + "error": ["ERROR", "CANCELED"], + "warning": ["BUILDING", "QUEUED", "INITIALIZING"], + "unknown": ["DELETED"] + } + } + ] + }, + { "name": "target", "displayName": "Target" }, + { "name": "projectId", "displayName": "Project ID", "visible": false }, + { + "name": "created", + "displayName": "Created", + "shape": "date", + "role": "timestamp" + }, + { "name": "url", "displayName": "Url", "shape": "url" }, + { + "name": "inspectorUrl", + "displayName": "Inspector Url", + "shape": "url" + }, + { "name": "creator", "displayName": "Creator" }, + { + "name": "ready", + "displayName": "Ready", + "shape": "date", + "role": "timestamp" + }, + { + "name": "buildingAt", + "displayName": "Building At", + "shape": "date", + "role": "timestamp" + }, + { + "name": "createdAt", + "displayName": "Created At", + "shape": "date", + "role": "timestamp" + }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/Vercel/v1/dataStreams/domainConfig.json b/plugins/Vercel/v1/dataStreams/domainConfig.json new file mode 100644 index 00000000..cd87e7a2 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/domainConfig.json @@ -0,0 +1,30 @@ +{ + "name": "domainConfig", + "displayName": "Domain Config", + "description": "Configuration health for a single Vercel domain — whether DNS/nameservers are misconfigured, the service type, and who configured it", + "tags": ["Domain"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v6/domains/{{object.name}}/config" + }, + "matches": { "sourceType": { "type": "equals", "value": "Domain" } }, + "metadata": [ + { + "name": "misconfigured", + "displayName": "Misconfigured", + "shape": "boolean" + }, + { + "name": "serviceType", + "displayName": "Service Type", + "shape": "string" + }, + { + "name": "configuredBy", + "displayName": "Configured By", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/domains.json b/plugins/Vercel/v1/dataStreams/domains.json new file mode 100644 index 00000000..860b80ec --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/domains.json @@ -0,0 +1,31 @@ +{ + "name": "domains", + "displayName": "Domains", + "description": "Lists Vercel custom domains in the configured account or team. Backs the Vercel Domain import and domain inventory tiles.", + "tags": [], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v5/domains", + "pathToData": "domains", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "limit", "value": "100" }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "until" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "visible": false }, + { "name": "sourceType", "computed": true, "valueExpression": "Vercel Domain", "visible": false }, + { "name": "name", "displayName": "Domain", "role": "label" }, + { "name": "verified", "displayName": "Verified" }, + { "name": "serviceType", "displayName": "Service Type" }, + { "name": "expiresAt", "displayName": "Expires", "shape": "date" }, + { "name": "boughtAt", "displayName": "Bought", "shape": "date" }, + { "name": "renew", "displayName": "Auto-renew" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/firewallEvents.json b/plugins/Vercel/v1/dataStreams/firewallEvents.json new file mode 100644 index 00000000..f77a4298 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/firewallEvents.json @@ -0,0 +1,69 @@ +{ + "name": "firewallEvents", + "displayName": "Firewall Events", + "description": "Per-action firewall event counts over the timeframe for a single Vercel project, one row per time-bucket and action type", + "tags": ["Security", "Firewall"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v1/security/firewall/events", + "getArgs": [ + { "key": "projectId", "value": "{{object.rawId}}" }, + { + "key": "startTimestamp", + "value": "{{timeframe.unixStart * 1000}}" + }, + { "key": "endTimestamp", "value": "{{timeframe.unixEnd * 1000}}" } + ], + "pathToData": "actions" + }, + "matches": { + "sourceType": { "type": "equals", "value": "Project" } + }, + "metadata": [ + { + "name": "startTime", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "action", + "displayName": "Action", + "shape": "string", + "role": "label" + }, + { + "name": "count", + "displayName": "Count", + "shape": "number", + "role": "value" + }, + { + "name": "host", + "displayName": "Host", + "shape": "string" + }, + { + "name": "public_ip", + "displayName": "Public IP", + "shape": "string" + }, + { + "name": "action_type", + "displayName": "Action Category", + "shape": "string", + "visible": false + }, + { + "name": "isActive", + "displayName": "Active", + "shape": "boolean", + "visible": false + }, + { + "pattern": ".*" + } + ], + "timeframes": true +} diff --git a/plugins/Vercel/v1/dataStreams/projectInfo.json b/plugins/Vercel/v1/dataStreams/projectInfo.json new file mode 100644 index 00000000..aa69f7ec --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/projectInfo.json @@ -0,0 +1,36 @@ +{ + "name": "projectInfo", + "displayName": "Project Info", + "description": "Per-project detail (framework, Node version, timestamps, git repo) for a single Vercel project", + "tags": ["Project"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v9/projects/{{object.rawId}}", + "expandInnerObjects": true + }, + "matches": { "sourceType": { "type": "equals", "value": "Project" } }, + "metadata": [ + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { "name": "framework", "displayName": "Framework", "shape": "string" }, + { + "name": "nodeVersion", + "displayName": "Node Version", + "shape": "string" + }, + { "name": "createdAt", "displayName": "Created", "shape": "date" }, + { "name": "updatedAt", "displayName": "Updated", "shape": "date" }, + { "name": "link.repo", "displayName": "Git Repo", "shape": "string" }, + { + "name": "link.type", + "displayName": "Git Provider", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/projects.json b/plugins/Vercel/v1/dataStreams/projects.json new file mode 100644 index 00000000..f15c053a --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/projects.json @@ -0,0 +1,43 @@ +{ + "name": "projects", + "displayName": "Projects", + "description": "Lists Vercel projects in the configured account or team. Backs the Vercel Project import and project inventory tiles.", + "tags": [], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v10/projects", + "pathToData": "projects", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "from" } + } + }, + "matches": "none", + "metadata": [ + { + "name": "id", + "displayName": "ID", + "role": "value", + "visible": false + }, + { + "name": "sourceType", + "computed": true, + "valueExpression": "Vercel Project", + "visible": false + }, + { "name": "name", "displayName": "Name", "role": "label" }, + { "name": "framework", "displayName": "Framework" }, + { "name": "nodeVersion", "displayName": "Node Version" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" }, + { "name": "updatedAt", "displayName": "Updated", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/scripts/cost.js b/plugins/Vercel/v1/dataStreams/scripts/cost.js new file mode 100644 index 00000000..32245cc8 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/scripts/cost.js @@ -0,0 +1,25 @@ +// GET /v1/billing/charges returns application/jsonl — newline-delimited JSON +// objects (FOCUS v1.3 cost/usage records), NOT a JSON array. The handler only +// auto-parses JSON/XML, so `data` is null/incomplete here; the raw text is on +// response.body. Split on newlines, drop empties, JSON.parse each line, then map +// each FOCUS record to a flat row with real JS primitives. +const records = (response.body || "") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => JSON.parse(line)); + +const num = (v) => (v === null || v === undefined || v === "" ? null : Number(v)); + +result = records.map((r) => { + const tags = r.Tags || {}; + return { + service: r.ServiceName, + billedCost: num(r.BilledCost), + effectiveCost: num(r.EffectiveCost), + quantity: num(r.ConsumedQuantity), + unit: r.ConsumedUnit, + projectName: tags.ProjectName || tags.ProjectId || null, + periodStart: r.ChargePeriodStart + }; +}); diff --git a/plugins/Vercel/v1/dataStreams/scripts/currentUser.js b/plugins/Vercel/v1/dataStreams/scripts/currentUser.js new file mode 100644 index 00000000..d294752f --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/scripts/currentUser.js @@ -0,0 +1,2 @@ +// /v2/user returns a single { user: {...} } object — wrap it as one row. +result = data && data.user ? [data.user] : []; diff --git a/plugins/Vercel/v1/dataStreams/scripts/deployments.js b/plugins/Vercel/v1/dataStreams/scripts/deployments.js new file mode 100644 index 00000000..18402b4e --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/scripts/deployments.js @@ -0,0 +1,37 @@ +// /v7/deployments returns { deployments: [...], pagination: {...} }. +// One row per deployment. Optional `project` objects-picker (stream ui name +// "project") arrives at context.config.project as an array (multi-select), each +// rawId a single-element array. Empty/absent => account-wide, no filter. +const deployments = (data && data.deployments) || []; + +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.project) || []; +const projectIds = new Set( + selected.map((o) => unwrap(o.rawId)).filter(Boolean), +); + +// Token paging walks `until` from pagination.next back to the `since` floor, so +// the first page starts at "now". For a historical timeframe (e.g. lastMonth) +// that over-fetches rows newer than the window end — bound the top here. +// (We can't set an `until` getArg: it collides with the paging `out=until`.) +const tf = context.timeframe || {}; +const untilMs = tf.unixEnd ? tf.unixEnd * 1000 : null; + +const scoped = deployments.filter( + (d) => + (!projectIds.size || projectIds.has(d.projectId)) && + (untilMs === null || d.created <= untilMs), +); + +result = scoped.map((d) => ({ + ...d, + uid: d.uid, + name: d.name, + state: d.readyState || d.state, + target: d.target || "preview", + projectId: d.projectId, + created: d.created, + inspectorUrl: d.inspectorUrl, + creator: (d.creator && (d.creator.username || d.creator.uid)) || null, +})); + diff --git a/plugins/Vercel/v1/dataStreams/teamMembers.json b/plugins/Vercel/v1/dataStreams/teamMembers.json new file mode 100644 index 00000000..d7284898 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/teamMembers.json @@ -0,0 +1,43 @@ +{ + "name": "teamMembers", + "displayName": "Team Members", + "description": "Lists members of the configured Vercel team", + "tags": ["Team"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v3/teams/{{dataSource.teamId}}/members", + "pathToData": "members", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "until" } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "ID", + "shape": "string", + "visible": false + }, + { "name": "name", "displayName": "Name", "shape": "string" }, + { + "name": "username", + "displayName": "Username", + "shape": "string", + "role": "label" + }, + { "name": "email", "displayName": "Email", "shape": "string" }, + { "name": "role", "displayName": "Role", "shape": "string" }, + { "name": "confirmed", "displayName": "Confirmed", "shape": "boolean" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/teams.json b/plugins/Vercel/v1/dataStreams/teams.json new file mode 100644 index 00000000..6acafe5f --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/teams.json @@ -0,0 +1,28 @@ +{ + "name": "teams", + "displayName": "Teams", + "description": "Lists the Vercel teams the configured token can access", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/teams", + "pathToData": "teams", + "expandInnerObjects": true, + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "limit", "value": "100" }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "until" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "visible": false }, + { "name": "name", "displayName": "Name", "role": "label" }, + { "name": "slug", "displayName": "Slug" }, + { "name": "membership.role", "displayName": "Your Role" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/defaultContent/activity.dash.json b/plugins/Vercel/v1/defaultContent/activity.dash.json new file mode 100644 index 00000000..b672b3c6 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/activity.dash.json @@ -0,0 +1,177 @@ +{ + "name": "Activity & Team", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "version": 115, + "contents": [ + { + "static": false, + "w": 3, + "moved": false, + "h": 3, + "x": 0, + "y": 0, + "i": "4c0a11bb-6c6b-481b-951c-fe4586313ea4", + "z": 0, + "config": { + "dataStream": { + "name": "activity", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[activity]}}", + "sort": { + "by": [["createdAt", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Account events", + "activePluginConfigIds": ["{{configId}}"], + "title": "Recent Activity", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "createdAt", + "type", + "text", + "actor" + ], + "hiddenColumns": [ + "id", + "user.username", + "user.email", + "userId" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 1, + "moved": false, + "h": 3, + "x": 3, + "y": 0, + "i": "9838709b-83d2-4542-85a7-6458352f4d93", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "teamMembers", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[teamMembers]}}", + "sort": { + "by": [["name", "asc"]] + } + }, + "_type": "tile/data-stream", + "description": "Members of the configured team", + "activePluginConfigIds": ["{{configId}}"], + "title": "Team Roster", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["username", "role"], + "hiddenColumns": [ + "uid", + "createdAt", + "email", + "name", + "confirmed" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 4, + "moved": false, + "h": 2, + "x": 0, + "y": 3, + "i": "fada223a-5939-4b55-bc61-3ef63deca7d2", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "teams", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[teams]}}", + "sort": { + "by": [["name", "asc"]] + } + }, + "_type": "tile/data-stream", + "description": "Teams accessible to the configured token", + "activePluginConfigIds": ["{{configId}}"], + "title": "Teams", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "slug", + "membership.role", + "createdAt" + ], + "hiddenColumns": ["id"], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 4, + "moved": false, + "h": 3, + "x": 0, + "y": 5, + "i": "34bbffe0-fdcd-479a-8c54-7757030d354a", + "z": 0, + "config": { + "dataStream": { + "name": "activity", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[activity]}}", + "sort": {}, + "group": { + "by": [["createdAt", "byDay"]], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Daily activity", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "seriesColumn": "none", + "xAxisColumn": "createdAt_byDay", + "yAxisColumn": ["count"] + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/cost.dash.json b/plugins/Vercel/v1/defaultContent/cost.dash.json new file mode 100644 index 00000000..d2eebea9 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/cost.dash.json @@ -0,0 +1,161 @@ +{ + "name": "Cost", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "contents": [ + { + "static": false, + "w": 2, + "moved": false, + "h": 4, + "x": 0, + "y": 0, + "i": "0f2bc2a4-cb0b-41a1-a4fb-9b0e29e8a088", + "z": 0, + "config": { + "timeframe": "last7days", + "dataStream": { + "name": "cost", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[cost]}}", + "sort": { + "by": [["billedCost_sum", "desc"]] + }, + "group": { + "by": [["service", "uniqueValues"]], + "aggregate": [ + { + "type": "sum", + "names": ["billedCost"] + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Billed cost grouped by service", + "activePluginConfigIds": ["{{configId}}"], + "title": "Cost by Service", + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "billedCost_sum", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table", + "labelColumn": "service_uniqueValues" + } + } + } + } + }, + { + "static": false, + "w": 2, + "moved": false, + "h": 4, + "x": 2, + "y": 0, + "i": "64bb0cfc-e350-43e6-a047-5fa04377cc10", + "z": 0, + "config": { + "timeframe": "last7days", + "dataStream": { + "name": "cost", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[cost]}}", + "sort": { + "top": 10, + "by": [["billedCost_sum", "desc"]] + }, + "group": { + "by": [["projectName", "uniqueValues"]], + "aggregate": [ + { + "type": "sum", + "names": ["billedCost"] + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Billed cost grouped by project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Cost by Project", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisGroup": "none", + "showLegend": false, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "projectName_uniqueValues", + "displayMode": "actual", + "yAxisLabel": "Billed Cost ($)", + "horizontalLayout": "horizontal", + "yAxisData": ["billedCost_sum"], + "showYAxisLabel": true, + "legendPosition": "bottom", + "showXAxisLabel": false + } + } + } + } + }, + { + "static": false, + "w": 4, + "moved": false, + "h": 3, + "x": 0, + "y": 4, + "i": "731acf15-16ed-4e1a-baa9-7f3d992e172f", + "z": 0, + "config": { + "timeframe": "last7days", + "dataStream": { + "name": "cost", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[cost]}}", + "sort": {}, + "group": { + "by": [["periodStart", "byDay"]], + "aggregate": [ + { + "type": "sum", + "names": ["billedCost"] + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Cost by day", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "seriesColumn": "none", + "dataPoints": true, + "showTrendLine": true, + "cumulative": false, + "xAxisColumn": "service_uniqueValues", + "yAxisColumn": ["billedCost_sum"] + } + } + } + } + } + ], + "version": 16, + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/deployments.dash.json b/plugins/Vercel/v1/defaultContent/deployments.dash.json new file mode 100644 index 00000000..ca646afd --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/deployments.dash.json @@ -0,0 +1,279 @@ +{ + "name": "Deployments", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "version": 79, + "contents": [ + { + "w": 2, + "h": 3, + "x": 0, + "y": 0, + "z": 0, + "i": "8880ebf2-9570-488b-ab5f-6cfeed0c1727", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [] + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "top": 10, + "by": [["created", "asc"]] + }, + "group": { + "by": [], + "aggregate": [] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Builds (Top 10)", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "state", + "name", + "url", + "created", + "source", + "readyState", + "readySubstate", + "type", + "creator", + "inspectorUrl", + "attribution.commitMeta.name", + "attribution.commitMeta.email", + "attribution.commitMeta.isVerified", + "attribution.gitUser.id", + "attribution.gitUser.login", + "attribution.gitUser.type", + "attribution.gitUser.provider", + "attribution.vercelUser.id", + "attribution.vercelUser.username", + "attribution.vercelUser.teamRoles.0", + "meta.githubCommitAuthorName", + "meta.githubCommitAuthorEmail", + "meta.githubCommitMessage", + "meta.githubCommitOrg", + "meta.githubCommitRef", + "meta.githubCommitRepo", + "meta.githubCommitSha", + "meta.githubDeployment", + "meta.githubOrg", + "meta.githubRepo", + "meta.githubRepoOwnerType", + "meta.githubCommitRepoId", + "meta.githubRepoId", + "meta.githubRepoVisibility", + "meta.githubHost", + "meta.githubCommitAuthorLogin", + "meta.githubCommitVerification", + "meta.repoPushedAt", + "meta.branchAlias", + "meta.lambdaRuntimeStats", + "target", + "aliasError", + "aliasAssigned", + "isRollbackCandidate", + "createdAt", + "buildingAt", + "ready", + "projectSettings.commandForIgnoringBuildStep", + "meta.githubPrId", + "state[Expanded].rawState" + ], + "hiddenColumns": ["uid", "projectId"] + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 0, + "z": 0, + "i": "69617b87-c49a-43a7-a4ec-4b60e2cdd16e", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "state", + "operation": "equals", + "value": "error" + } + ] + }, + "id": "{{dataStreams.[deployments]}}", + "group": { + "by": [], + "aggregate": [] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Failed Builds", + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "stateColumn": "state", + "linkColumn": "inspectorUrl", + "sublabel": "url", + "labelColumn": "name" + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 0, + "y": 3, + "z": 0, + "i": "ece1c11a-be71-4a6f-a1f2-ed56c15081d7", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created_byDay", "asc"]] + }, + "group": { + "by": [ + ["created", "byDay"], + ["state", "uniqueValues"] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Deployments per day, split by state", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployments per day", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "color": { + "type": "default" + }, + "xAxisGroup": "state_uniqueValues", + "showLegend": true, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "created_byDay", + "displayMode": "actual", + "showTotals": true, + "yAxisLabel": "Deployments", + "horizontalLayout": "vertical", + "showValue": false, + "yAxisData": ["count"], + "showYAxisLabel": true, + "xAxisLabel": "", + "legendPosition": "bottom", + "showXAxisLabel": false + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 3, + "z": 0, + "i": "d2eaaf90-d384-478a-b14d-dc955bbdeb3e", + "moved": false, + "static": false, + "config": { + "title": "Average build times", + "description": "", + "_type": "tile/data-stream", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "WITH\r\n \"build_times\" AS (\r\n SELECT\r\n STRFTIME(\r\n DATE_TRUNC('day', \"createdAt\"),\r\n '%Y-%m-%dT%H:%M:%SZ'\r\n ) AS \"date\",\r\n \"name\",\r\n EXTRACT (\r\n EPOCH\r\n FROM\r\n \"buildingAt\" - \"createdAt\"\r\n ) AS \"build_time\"\r\n FROM\r\n \"dataset1\"\r\n )\r\nSELECT\r\n \"date\",\r\n \"name\",\r\n AVG(\"build_time\") AS \"avg_build_time\"\r\nFROM\r\n \"build_times\"\r\nGROUP BY\r\n \"date\",\r\n \"name\"", + "tables": [ + { + "tableName": "dataset1", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments" + } + } + } + ] + }, + "metadata": [ + { + "shape": [ + "seconds", + { + "thousandsSeparator": true, + "formatDuration": true + } + ], + "name": "avg_build_time" + }, + { + "pattern": ".*" + } + ] + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": ["avg_build_time"], + "seriesColumn": "name", + "showLegend": true, + "legendPosition": "bottom", + "dataPoints": true + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/domain.dash.json b/plugins/Vercel/v1/defaultContent/domain.dash.json new file mode 100644 index 00000000..5e8cb0a8 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/domain.dash.json @@ -0,0 +1,98 @@ +{ + "name": "Domain", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Vercel Domain]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "2d1a775f-5d77-4246-a727-47b4b22c0cc7", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Configuration Health", + "description": "DNS / nameserver configuration for this domain", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[domainConfig]}}", + "name": "domainConfig", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Vercel Domains]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Vercel Domain]}}" + }, + "variables": ["{{variables.[Vercel Domain]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["misconfigured", "serviceType", "configuredBy"] + } + } + }, + "monitorOld": { + "_type": "simple", + "monitorType": "threshold", + "aggregation": "count", + "groupBy": "__group_by_none__", + "frequency": 15, + "tileRollsUp": true, + "condition": { + "columns": ["misconfigured"], + "logic": { "if": [{ "==": [{ "var": "misconfigured" }, true] }, "error"] } + } + } + } + }, + { + "i": "da5c6f6d-e225-4de1-90c8-873e444e2fb2", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Domain Details", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Vercel Domains]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Vercel Domain]}}" + }, + "variables": ["{{variables.[Vercel Domain]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "hiddenColumns": ["id", "link", "links", "label"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Vercel/v1/defaultContent/manifest.json b/plugins/Vercel/v1/defaultContent/manifest.json new file mode 100644 index 00000000..0fcc169c --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/manifest.json @@ -0,0 +1,10 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "cost", "type": "dashboard" }, + { "name": "deployments", "type": "dashboard" }, + { "name": "activity", "type": "dashboard" }, + { "name": "project", "type": "dashboard" }, + { "name": "domain", "type": "dashboard" } + ] +} diff --git a/plugins/Vercel/v1/defaultContent/overview.dash.json b/plugins/Vercel/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..80f7ada6 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/overview.dash.json @@ -0,0 +1,305 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "version": 95, + "contents": [ + { + "static": false, + "w": 1, + "moved": false, + "h": 3, + "x": 0, + "y": 0, + "i": "fa9e69dc-7d70-450b-b973-1a215df3a398", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "projects", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projects]}}", + "sort": { + "by": [["count", "desc"]] + }, + "group": { + "by": [["framework", "uniqueValues"]], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Projects by Framework", + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table", + "labelColumn": "framework_uniqueValues" + } + } + } + } + }, + { + "static": false, + "w": 3, + "moved": false, + "h": 3, + "x": 1, + "y": 0, + "i": "3850d290-308d-44a1-81de-aeff5200d0ba", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "projects", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projects]}}", + "sort": { + "by": [["updatedAt", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Projects", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "framework", + "nodeVersion", + "updatedAt" + ], + "hiddenColumns": [ + "id", + "sourceType", + "createdAt" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 1, + "moved": false, + "h": 3, + "x": 0, + "y": 3, + "i": "f3ff120e-5ed1-414f-b57e-a2982d25ebbe", + "z": 0, + "config": { + "timeframe": "last7days", + "dataStream": { + "name": "cost", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[cost]}}", + "sort": { + "top": 10, + "by": [] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "sum", + "names": ["billedCost"] + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Overall Cost", + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "billedCost_sum", + "comparisonColumn": "none" + } + } + } + } + }, + { + "static": false, + "w": 3, + "moved": false, + "h": 3, + "x": 1, + "y": 3, + "i": "098760ae-37bf-485c-8b14-605051f34733", + "z": 0, + "config": { + "timeframe": "last7days", + "dataStream": { + "name": "cost", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[cost]}}", + "sort": { + "top": 10, + "by": [["billedCost_sum", "desc"]] + }, + "group": { + "by": [["projectName", "uniqueValues"]], + "aggregate": [ + { + "type": "sum", + "names": ["billedCost"] + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Billed cost grouped by project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Cost by Project", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "color": { + "type": "default" + }, + "xAxisGroup": "none", + "showLegend": false, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "projectName_uniqueValues", + "displayMode": "actual", + "showTotals": false, + "yAxisLabel": "Billed Cost ($)", + "horizontalLayout": "horizontal", + "showValue": false, + "yAxisData": ["billedCost_sum"], + "showYAxisLabel": true, + "xAxisLabel": "", + "legendPosition": "bottom", + "showXAxisLabel": false + } + } + } + } + }, + { + "static": false, + "w": 1, + "moved": false, + "h": 2, + "x": 0, + "y": 6, + "i": "60fe71cb-be1f-4627-9eb7-c4aa7fe10feb", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "domains", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "verified", + "operation": "equals", + "value": "false" + } + ] + }, + "id": "{{dataStreams.[domains]}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Domains that are not verified", + "activePluginConfigIds": ["{{configId}}"], + "title": "Unverified Domains", + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Unverified" + } + } + } + } + }, + { + "static": false, + "w": 3, + "moved": false, + "h": 2, + "x": 1, + "y": 6, + "i": "f2843ac7-c436-4ad4-a6c8-5e5cb570420b", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "domains", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[domains]}}", + "sort": { + "by": [["expiresAt", "asc"]] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Domain Inventory", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "verified", + "serviceType", + "renew", + "expiresAt" + ], + "hiddenColumns": [ + "id", + "sourceType", + "boughtAt", + "createdAt" + ], + "transpose": false + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/project.dash.json b/plugins/Vercel/v1/defaultContent/project.dash.json new file mode 100644 index 00000000..95714503 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/project.dash.json @@ -0,0 +1,356 @@ +{ + "name": "Project", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Vercel Project]}}"], + "dashboard": { + "_type": "layout/grid", + "version": 29, + "contents": [ + { + "w": 1, + "h": 2, + "x": 0, + "y": 0, + "z": 0, + "i": "05a07731-9c30-4908-95b4-94f9f345d54c", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "top": 1, + "by": [["created", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Most recent deployment for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Deployment State", + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "stateColumn": "state", + "linkColumn": "inspectorUrl", + "sublabel": "target", + "columns": 1, + "labelColumn": "name" + } + } + } + } + }, + { + "w": 1, + "h": 2, + "x": 1, + "y": 0, + "z": 0, + "i": "d04fd358-ff9f-41f2-a7ea-5398a4cd34b9", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "filters": [ + { + "column": "state", + "operation": "equals", + "value": "error" + } + ], + "multiOperation": "or" + }, + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "group": { + "by": [], + "aggregate": [] + }, + "sort": {} + }, + "_type": "tile/data-stream", + "description": "Deployments in an error state for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Failed Deployments", + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { + "type": "count" + }, + "comparisonColumn": "none", + "label": "Failed" + } + } + } + } + }, + { + "w": 2, + "h": 2, + "x": 2, + "y": 0, + "z": 0, + "i": "508189d0-5357-4722-8701-09b082e20c35", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created_byDay", "asc"]] + }, + "group": { + "by": [ + ["created", "byDay"], + ["state", "uniqueValues"] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Deployments per day for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployment Volume", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisGroup": "state_uniqueValues", + "showLegend": false, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "created_byDay", + "displayMode": "actual", + "yAxisLabel": "Deployments", + "horizontalLayout": "vertical", + "yAxisData": ["count"], + "showYAxisLabel": true, + "legendPosition": "bottom", + "showXAxisLabel": false, + "showValue": false, + "xAxisLabel": "", + "color": { + "type": "default" + }, + "showTotals": true + } + } + } + } + }, + { + "w": 4, + "h": 3, + "x": 0, + "y": 2, + "z": 0, + "i": "b2a0e5d2-746d-48e2-b294-0cd82105f86c", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Deployments for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployment History", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "created", + "name", + "state", + "target", + "creator", + "inspectorUrl" + ], + "hiddenColumns": ["uid", "projectId"], + "transpose": false + } + } + } + } + }, + { + "w": 1, + "h": 3, + "x": 0, + "y": 5, + "z": 0, + "i": "378b1b08-9b44-4564-a83f-f59f2d02b9f8", + "moved": false, + "static": false, + "config": { + "timeframe": "none", + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "projectInfo", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projectInfo]}}" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Project Info", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "framework", + "nodeVersion", + "link.repo", + "link.type", + "createdAt", + "updatedAt" + ], + "transpose": true + } + } + } + } + }, + { + "w": 1, + "h": 3, + "x": 1, + "y": 5, + "z": 0, + "i": "6a4aa9a9-0870-452b-a66b-83917f0641da", + "moved": false, + "static": false, + "config": { + "title": "Firewall Event Actions", + "description": "", + "_type": "tile/data-stream", + "dataStream": { + "id": "{{dataStreams.[firewallEvents]}}", + "name": "firewallEvents" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "action" + } + } + }, + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Vercel Project]}}"] + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 5, + "z": 0, + "i": "97508e56-5d5b-4716-9223-8a3903c5d3da", + "moved": false, + "static": false, + "config": { + "title": "Firewall events", + "description": "", + "_type": "tile/data-stream", + "dataStream": { + "id": "{{dataStreams.[firewallEvents]}}", + "name": "firewallEvents" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "hiddenColumns": ["isActive", "action_type"], + "columnOrder": [ + "action", + "count", + "host", + "startTime", + "endTime", + "public_ip" + ] + } + } + }, + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Vercel Project]}}"] + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/scopes.json b/plugins/Vercel/v1/defaultContent/scopes.json new file mode 100644 index 00000000..da9b2912 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/scopes.json @@ -0,0 +1,26 @@ +[ + { + "name": "Vercel Projects", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Project"] } + }, + "variable": { + "name": "Vercel Project", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Vercel Domains", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Domain"] } + }, + "variable": { + "name": "Vercel Domain", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Vercel/v1/docs/README.md b/plugins/Vercel/v1/docs/README.md new file mode 100644 index 00000000..84047b70 --- /dev/null +++ b/plugins/Vercel/v1/docs/README.md @@ -0,0 +1,55 @@ +## What this plugin monitors + +Monitor your [Vercel](https://vercel.com) projects, deployments, and domains in SquaredUp. This plugin connects to the Vercel REST API to import your projects and domains as objects you can scope dashboards to, and provides data streams for deployment health and history, domain verification and expiry, team membership and activity, and a billing-based cost overview. + +- **Projects** — your Vercel projects, including framework, git repository, and latest production deployment status. Imported as objects. +- **Domains** — custom domains, including verification status, expiry/renewal dates, and configuration health. Imported as objects. +- **Deployments** — deployment volume, success/failure rates, and history over time, account-wide or drilled down per project. (Deployments are _not_ indexed as objects, because they change too frequently — they are available as data streams instead.) +- **Team & activity** — team membership roster and recent account activity (events). +- **Cost** — a cost/usage overview derived from Vercel billing data (daily granularity). +- **Firewall / security** — per-project [Vercel Firewall (WAF)](https://vercel.com/docs/vercel-firewall) event activity: counts of firewall events broken down by action (e.g. allow / deny / challenge) over time. Available as data streams on the **Project** perspective's **Security** section. + +The plugin ships with three out-of-the-box dashboards: an account **Overview**, a **Project** perspective (which includes a **Security** section), and a **Domain** perspective. + +## Prerequisites + +1. Sign in to Vercel and open **Account Settings → Tokens** (). +2. Click **Create Token**. +3. Give it a name (e.g. `SquaredUp`). +4. **Scope** — choose the scope the token can access: + - To monitor a **Team**, set the scope to that team and note the team's ID or slug (see the `Team ID` field below). + - To monitor your **personal account**, scope it to your personal account. +5. **Expiration** — choose an expiration (or no expiration). If the token expires, the plugin's data streams will stop returning data until you supply a new token. +6. Click **Create** and copy the token value immediately — Vercel only shows it once. + +For the **cost overview** and **team members** streams, the token must belong to a Team and carry a role with billing/member visibility (Owner, Member, Developer, Security, or Billing). On personal/Hobby accounts these streams may return no data. + +### Finding your Team ID + +If you are monitoring a Team, open **Team Settings → General** in Vercel; the **Team ID** (format `team_xxxxxxxx`) is shown there. Enter that value in the `Team ID` field. (Leave the field blank to monitor your personal account instead.) + +## Configuration fields + +| Field | Required | Description | Where to find it | +| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| **API Token** | Yes | The Vercel Access Token used to authenticate. Sent as a bearer token on every request. | Account Settings → Tokens (see above). | +| **Team ID** | No | The Team ID or slug to monitor. Leave blank to monitor your personal account. When set, all requests are scoped to this team. | Team Settings → General (`team_…`), or the slug in your Vercel URL. | + +## What gets indexed + +The plugin imports two object types into the SquaredUp graph: + +- **Vercel Project** — one object per project in the configured account/team. Carries the project name, framework, git repository link, and identifiers. +- **Vercel Domain** — one object per custom domain. Carries the domain name, verification status, and expiry information. + +Deployments, teams, members, activity events, and cost data are provided as **data streams** (not indexed objects) — query them on dashboards and scope deployment streams to a project. + +## Known limitations + +- **Deployments are not indexed.** They are available only as data streams, because deployment volume and churn make them unsuitable as long-lived graph objects. +- **No real-time analytics or metrics via REST.** Vercel does **not** expose Web Analytics (pageviews/visitors), Speed Insights (Core Web Vitals), or real-time Observability metrics (edge requests, function invocations, bandwidth) through its public REST API. Operational usage is available only as **daily billed quantities** via the cost stream — not real-time, per-request telemetry. +- **Cost data is daily granularity** and the billing endpoint returns very large datasets, so the cost stream is restricted to the **Last 24 hours** and **Last 7 days** timeframes. +- **Team members and cost require a Team** and a token with adequate role/plan. On personal/Hobby accounts these streams may be empty. +- **Firewall events only appear for projects with the WAF configured.** The Firewall events stream returns rows only for projects that have the Vercel Firewall set up and that recorded events within the selected timeframe; a project with no firewall activity in that window shows an empty result (not an error). +- **One connection = one scope.** Each configured plugin instance monitors either your personal account or a single team. To monitor multiple teams, add the plugin once per team. +- **Rate limits.** Vercel enforces per-action rate limits (HTTP 429). Very large accounts may occasionally see throttling during imports. diff --git a/plugins/Vercel/v1/icon.svg b/plugins/Vercel/v1/icon.svg new file mode 100644 index 00000000..73961ba7 --- /dev/null +++ b/plugins/Vercel/v1/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/Vercel/v1/indexDefinitions/default.json b/plugins/Vercel/v1/indexDefinitions/default.json new file mode 100644 index 00000000..a07b6dc4 --- /dev/null +++ b/plugins/Vercel/v1/indexDefinitions/default.json @@ -0,0 +1,28 @@ +{ + "steps": [ + { + "name": "projects", + "dataStream": { "name": "projects" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Project" + } + } + }, + { + "name": "domains", + "dataStream": { "name": "domains" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Domain" + } + } + } + ] +} diff --git a/plugins/Vercel/v1/metadata.json b/plugins/Vercel/v1/metadata.json new file mode 100644 index 00000000..a8be3c4f --- /dev/null +++ b/plugins/Vercel/v1/metadata.json @@ -0,0 +1,48 @@ +{ + "name": "vercel", + "displayName": "Vercel", + "version": "1.0.0", + "author": { "name": "@andrewmumblebee", "type": "community" }, + "description": "Monitor your Vercel projects, deployments, and domains in SquaredUp — track deployment health and history, domain verification and expiry, team activity, a cost overview, and firewall (WAF) event activity.", + "category": "Cloud Platforms", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "vercel", + "deployments", + "hosting", + "frontend", + "projects", + "domains", + "security", + "firewall", + "waf" + ], + "objectTypes": ["Project", "Domain"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Vercel/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Vercel", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.vercel.com", + "authMode": "none", + "headers": [ + { "key": "Authorization", "value": "Bearer {{accessToken}}" } + ], + "queryArgs": [{ "key": "teamId", "value": "{{teamId}}" }] + } + } +} diff --git a/plugins/Vercel/v1/ui.json b/plugins/Vercel/v1/ui.json new file mode 100644 index 00000000..86f3d3b9 --- /dev/null +++ b/plugins/Vercel/v1/ui.json @@ -0,0 +1,21 @@ +[ + { + "type": "markdown", + "name": "info", + "content": "Create a Vercel **Access Token** in [Account Settings → Tokens](https://vercel.com/account/tokens) and paste it below. To monitor a Team, also provide its **Team ID**; leave it blank to monitor your personal account." + }, + { + "type": "password", + "name": "accessToken", + "label": "API Token", + "help": "A Vercel Access Token. Sent as a bearer token on every request. Create one at [Account Settings → Tokens](https://vercel.com/account/tokens).", + "validation": { "required": true } + }, + { + "type": "text", + "name": "teamId", + "label": "Team ID", + "placeholder": "team_xxxxxxxxxxxxxxxx", + "help": "Optional. The ID of the Vercel Team to monitor (found at Team Settings → General, format `team_…`). Leave blank to monitor your personal account." + } +]