diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c44719cf..d689d1fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ plugins/AutoTask/* @TimWheeler-SQUP plugins/DattoRMM/* @TimWheeler-SQUP plugins/DigiCert/* @shaswot77 plugins/Huntress/* @Deenk +plugins/IncidentIO/ @noorulkhan-n plugins/FantasyPremierLeague/* @TimWheeler-SQUP plugins/GoogleSheets/* @kieranlangton plugins/MetOffice/* @blackgrouse diff --git a/plugins/IncidentIO/v1/configValidation.json b/plugins/IncidentIO/v1/configValidation.json new file mode 100644 index 00000000..a3536b99 --- /dev/null +++ b/plugins/IncidentIO/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "currentUser" }, + "required": true, + "error": "Could not authenticate. Check your API key is valid and has not expired.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/IncidentIO/v1/custom_types.json b/plugins/IncidentIO/v1/custom_types.json new file mode 100644 index 00000000..e87d1a38 --- /dev/null +++ b/plugins/IncidentIO/v1/custom_types.json @@ -0,0 +1,23 @@ +[ + { + "name": "Schedule", + "sourceType": "Schedule", + "icon": "calendar-days", + "singular": "Schedule", + "plural": "Schedules" + }, + { + "name": "Team", + "sourceType": "Team", + "icon": "people-group", + "singular": "Team", + "plural": "Teams" + }, + { + "name": "User", + "sourceType": "User", + "icon": "user", + "singular": "User", + "plural": "Users" + } +] diff --git a/plugins/IncidentIO/v1/dataStreams/alerts.json b/plugins/IncidentIO/v1/dataStreams/alerts.json new file mode 100644 index 00000000..dd70891e --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/alerts.json @@ -0,0 +1,32 @@ +{ + "name": "alerts", + "displayName": "Alerts", + "description": "All alerts ingested by alert sources, one row per alert", + "tags": ["Alerts"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/alerts", + "pathToData": "alerts", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "50" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "title", "displayName": "Title", "shape": "string", "role": "label" }, + { "name": "status", "displayName": "Status", "shape": "string" }, + { "name": "alert_source_id", "displayName": "Alert Source ID", "shape": "string" }, + { "name": "deduplication_key", "displayName": "Deduplication Key", "shape": "string" }, + { "name": "created_at", "displayName": "Created At", "shape": "date" }, + { "name": "updated_at", "displayName": "Updated At", "shape": "date" }, + { "name": "resolved_at", "displayName": "Resolved At", "shape": "date" }, + { "name": "source_url", "displayName": "Source URL", "shape": "url" }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/currentUser.json b/plugins/IncidentIO/v1/dataStreams/currentUser.json new file mode 100644 index 00000000..e18f177d --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/currentUser.json @@ -0,0 +1,15 @@ +{ + "name": "currentUser", + "displayName": "Current User", + "description": "Identity and role details for the configured API key", + "tags": ["Users"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/identity" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [{ "pattern": ".*" }], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/incidents.json b/plugins/IncidentIO/v1/dataStreams/incidents.json new file mode 100644 index 00000000..b42c7973 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/incidents.json @@ -0,0 +1,57 @@ +{ + "name": "incidents", + "displayName": "Incidents", + "description": "All incidents in the organization within the selected timeframe, one row per incident", + "tags": ["Incidents"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/incidents", + "expandInnerObjects": true, + "getArgs": [ + { + "key": "created_at[date_range]", + "value": "{{timeframe.enum === 'none' ? null : (timeframe.start + '~' + timeframe.end)}}" + } + ], + "pathToData": "incidents", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "reference", "displayName": "Reference", "shape": "string" }, + { "name": "mode", "displayName": "Mode", "shape": "string" }, + { "name": "visibility", "displayName": "Visibility", "shape": "string" }, + { "name": "incident_status.name", "displayName": "Status", "shape": "string" }, + { "name": "incident_status.category", "displayName": "Status Category", "shape": "string" }, + { "name": "severity.name", "displayName": "Severity", "shape": "string" }, + { "name": "incident_type.name", "displayName": "Type", "shape": "string" }, + { "name": "created_at", "displayName": "Created At", "shape": "date" }, + { "name": "updated_at", "displayName": "Updated At", "shape": "date" }, + { "name": "permalink", "displayName": "Permalink", "shape": "url" }, + { "pattern": ".*" } + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", + "none" + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" +} diff --git a/plugins/IncidentIO/v1/dataStreams/scheduleOnCall.json b/plugins/IncidentIO/v1/dataStreams/scheduleOnCall.json new file mode 100644 index 00000000..982a61d3 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/scheduleOnCall.json @@ -0,0 +1,26 @@ +{ + "name": "scheduleOnCall", + "displayName": "Schedule On-Call", + "description": "Who is currently on-call for a schedule, one row per active shift/layer", + "tags": ["On-Call", "Schedules"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v2/schedules/{{object.rawId}}", + "pathToData": "schedule.current_shifts", + "expandInnerObjects": true + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Schedule"] } }, + "metadata": [ + { "name": "user.name", "displayName": "On-Call User", "shape": "string", "role": "label" }, + { "name": "user.email", "displayName": "Email", "shape": "string" }, + { "name": "start_at", "displayName": "Start At", "shape": "date", "role": "timestamp" }, + { "name": "end_at", "displayName": "End At", "shape": "date" }, + { "name": "rotation_id", "displayName": "Rotation ID", "shape": "string" }, + { "name": "layer_id", "displayName": "Layer ID", "shape": "string" }, + { "name": "entry_id", "displayName": "Entry ID", "shape": "string", "visible": false }, + { "name": "fingerprint", "displayName": "Fingerprint", "shape": "string", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/scheduleShifts.json b/plugins/IncidentIO/v1/dataStreams/scheduleShifts.json new file mode 100644 index 00000000..46f39e84 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/scheduleShifts.json @@ -0,0 +1,32 @@ +{ + "name": "scheduleShifts", + "displayName": "Schedule Shifts", + "description": "The schedule's effective shifts across a time window, accounting for overrides", + "tags": ["On-Call", "Schedules"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v2/schedule_entries", + "getArgs": [ + { "key": "schedule_id", "value": "{{object.rawId}}" }, + { "key": "entry_window_start", "value": "{{timeframe.start}}" }, + { "key": "entry_window_end", "value": "{{timeframe.end}}" } + ], + "expandInnerObjects": true, + "pathToData": "schedule_entries.final", + "paging": { "mode": "none" } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Schedule"] } }, + "metadata": [ + { "name": "user.name", "displayName": "On-Call User", "shape": "string", "role": "label" }, + { "name": "user.email", "displayName": "Email", "shape": "string" }, + { "name": "start_at", "displayName": "Start", "shape": "date", "role": "timestamp" }, + { "name": "end_at", "displayName": "End", "shape": "date" }, + { "name": "rotation_id", "displayName": "Rotation ID", "shape": "string" }, + { "name": "layer_id", "displayName": "Layer ID", "shape": "string" }, + { "name": "entry_id", "displayName": "Entry ID", "shape": "string", "visible": false }, + { "name": "fingerprint", "displayName": "Fingerprint", "shape": "string", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/IncidentIO/v1/dataStreams/schedules.json b/plugins/IncidentIO/v1/dataStreams/schedules.json new file mode 100644 index 00000000..ba2db41f --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/schedules.json @@ -0,0 +1,32 @@ +{ + "name": "schedules", + "displayName": "Schedules", + "description": "All on-call schedules in the organization, one row per schedule", + "tags": ["On-Call", "Schedules"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/schedules", + "postRequestScript": "schedules.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "timezone", "displayName": "Timezone", "shape": "string" }, + { "name": "rotationCount", "displayName": "Rotations", "shape": "number" }, + { "name": "members", "displayName": "Members", "shape": "string" }, + { "name": "memberCount", "displayName": "Member Count", "shape": "number", "visible": false }, + { "name": "created_at", "displayName": "Created At", "shape": "date" }, + { "name": "updated_at", "displayName": "Updated At", "shape": "date" }, + { "name": "permalink", "displayName": "Permalink", "shape": "url" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/scripts/schedules.js b/plugins/IncidentIO/v1/dataStreams/scripts/schedules.js new file mode 100644 index 00000000..5bb0bda0 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/scripts/schedules.js @@ -0,0 +1,23 @@ +// dataStreams/scripts/schedules.js +// The raw response nests rotation membership two levels deep +// (schedule.config.rotations[].users[]), which pathToData/valueExpression can't +// reach - flatten it into a readable, deduplicated list of member names. +result = (data.schedules || []).map((schedule) => { + const rotations = (schedule.config && schedule.config.rotations) || []; + const members = new Map(); + rotations.forEach((rotation) => + (rotation.users || []).forEach((user) => members.set(user.id, user.name)), + ); + + return { + id: schedule.id, + name: schedule.name, + timezone: schedule.timezone, + rotationCount: rotations.length, + members: [...members.values()].join(", "), + memberCount: members.size, + created_at: schedule.created_at, + updated_at: schedule.updated_at, + permalink: schedule.permalink, + }; +}); diff --git a/plugins/IncidentIO/v1/dataStreams/scripts/teams.js b/plugins/IncidentIO/v1/dataStreams/scripts/teams.js new file mode 100644 index 00000000..b917b8aa --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/scripts/teams.js @@ -0,0 +1,10 @@ +// dataStreams/scripts/teams.js +// The v3/teams response nests a `members` array per team. A metadata +// valueExpression cannot compute a length from a sibling array/object +// column (cross-column references receive an already-shaped placeholder, +// not the real array), so the member count is derived here instead, +// against the untouched raw response. +result = (data.teams || []).map((team) => ({ + ...team, + memberCount: (team.members || []).length, +})); diff --git a/plugins/IncidentIO/v1/dataStreams/scripts/userSchedules.js b/plugins/IncidentIO/v1/dataStreams/scripts/userSchedules.js new file mode 100644 index 00000000..305405d3 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/scripts/userSchedules.js @@ -0,0 +1,30 @@ +// dataStreams/scripts/userSchedules.js +// Cross-references the schedules list against the scoped user's id: for each +// schedule, filters current_shifts to the ones where the shift's on-call +// user matches, and emits one row per matching (schedule, shift) pair. This +// can't be expressed with pathToData/valueExpression alone because it joins +// a nested per-schedule array against the scope object's id. +const userId = context.objects[0].rawId; + +// This script only ever sees ONE page of `schedules` per invocation (`data` is that +// page's raw response body) — but the platform re-invokes it once per page as the +// `paging` config advances the `after` cursor, and concatenates every invocation's +// `result` into the stream's final output. Filtering per page and letting the +// platform concatenate is equivalent to concatenating all pages before filtering, +// since this is a stateless per-row filter with no cross-page aggregation — so every +// schedule, on every page, is checked. Confirmed empirically against this endpoint by +// forcing page_size=1 (2 pages for 2 real schedules) and seeing both schedules' +// shifts in the final result, each tagged with its own page's cursor. +result = (data.schedules || []).flatMap((schedule) => + (schedule.current_shifts || []) + .filter((shift) => shift.user && shift.user.id === userId) + .map((shift) => ({ + scheduleId: schedule.id, + scheduleName: schedule.name, + schedulePermalink: schedule.permalink, + start_at: shift.start_at, + end_at: shift.end_at, + rotation_id: shift.rotation_id || null, + layer_id: shift.layer_id || null, + })), +); diff --git a/plugins/IncidentIO/v1/dataStreams/teams.json b/plugins/IncidentIO/v1/dataStreams/teams.json new file mode 100644 index 00000000..75533e10 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/teams.json @@ -0,0 +1,28 @@ +{ + "name": "teams", + "displayName": "Teams", + "description": "All teams in the organization, one row per team", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v3/teams", + "postRequestScript": "teams.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "members", "displayName": "Members", "shape": "json", "visible": false }, + { "name": "memberCount", "displayName": "Member Count", "shape": "number" }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/userSchedules.json b/plugins/IncidentIO/v1/dataStreams/userSchedules.json new file mode 100644 index 00000000..11171172 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/userSchedules.json @@ -0,0 +1,29 @@ +{ + "name": "userSchedules", + "displayName": "User Schedules", + "description": "Schedules the user currently appears in as on-call, one row per matching shift", + "tags": ["On-Call", "Users"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v2/schedules", + "postRequestScript": "userSchedules.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["User"] } }, + "metadata": [ + { "name": "scheduleId", "displayName": "Schedule ID", "shape": "string", "visible": false }, + { "name": "scheduleName", "displayName": "Schedule", "shape": "string", "role": "label" }, + { "name": "schedulePermalink", "displayName": "Schedule Link", "shape": "url" }, + { "name": "start_at", "displayName": "Start", "shape": "date", "role": "timestamp" }, + { "name": "end_at", "displayName": "End", "shape": "date" }, + { "name": "rotation_id", "displayName": "Rotation ID", "shape": "string" }, + { "name": "layer_id", "displayName": "Layer ID", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/dataStreams/users.json b/plugins/IncidentIO/v1/dataStreams/users.json new file mode 100644 index 00000000..9fd56ba2 --- /dev/null +++ b/plugins/IncidentIO/v1/dataStreams/users.json @@ -0,0 +1,33 @@ +{ + "name": "users", + "displayName": "Users", + "description": "All users in the organization, one row per user", + "tags": ["Users"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/users", + "expandInnerObjects": true, + "pathToData": "users", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "pagination_meta.after" }, + "out": { "realm": "queryArg", "path": "after" } + } + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "email", "displayName": "Email", "shape": "string" }, + { "name": "role", "displayName": "Role", "shape": "string" }, + { "name": "is_active", "displayName": "Active", "shape": "boolean" }, + { "name": "slack_user_id", "displayName": "Slack User ID", "shape": "string" }, + { "name": "seats.on_call", "displayName": "On-Call Seat", "shape": "string" }, + { "name": "seats.response", "displayName": "Response Seat", "shape": "string" }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/IncidentIO/v1/defaultContent/manifest.json b/plugins/IncidentIO/v1/defaultContent/manifest.json new file mode 100644 index 00000000..e99f07c5 --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/manifest.json @@ -0,0 +1,8 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "schedulePerspective", "type": "dashboard" }, + { "name": "teamPerspective", "type": "dashboard" }, + { "name": "userPerspective", "type": "dashboard" } + ] +} diff --git a/plugins/IncidentIO/v1/defaultContent/overview.dash.json b/plugins/IncidentIO/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..a039d4d3 --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/overview.dash.json @@ -0,0 +1,474 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "4a85c50c-12a8-4088-aaad-0314febc841c", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Incidents", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Total Incidents" + } + } + } + } + }, + { + "i": "1e9416d4-423e-4ef3-a25e-b420000e4f22", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Alerts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[alerts]}}", + "name": "alerts", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Total Alerts" + } + } + } + } + }, + { + "i": "56555b98-b97a-4474-bf36-49e157235e70", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Schedules", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "scope": { + "scope": "{{scopes.[Schedules]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Total Schedules" + } + } + } + } + }, + { + "i": "8ae18f2d-080f-4f8d-adf3-37ce50cee107", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Teams", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "scope": { + "scope": "{{scopes.[Teams]}}", + "workspace": "{{workspaceId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Total Teams" + } + } + } + } + }, + { + "i": "3b6c1a2d-9e4f-4a1b-8c3d-5f7e2a9b4c6e", + "x": 0, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open Incidents", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { "column": "incident_status.category", "operation": "notequals", "value": "closed" }, + { "column": "incident_status.category", "operation": "notequals", "value": "declined" }, + { "column": "incident_status.category", "operation": "notequals", "value": "canceled" }, + { "column": "incident_status.category", "operation": "notequals", "value": "merged" } + ] + }, + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Open Incidents" + } + } + } + } + }, + { + "i": "7d2e8f1c-4b5a-4e9d-b6c8-1a3f5d7e9b2c", + "x": 2, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Firing Alerts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[alerts]}}", + "name": "alerts", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [{ "column": "status", "operation": "equals", "value": "firing" }] + }, + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Firing Alerts" + } + } + } + } + }, + { + "i": "20dbbbe4-d77a-464e-840c-c7d2d7af97be", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents by Status", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["incident_status.name", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "incident_status.name_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "ce9049e7-1621-464e-bc4d-b95b5254fbbf", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents by Severity", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["severity.name", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "severity.name_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "88851ce4-a8e9-49b6-be33-dde1f8fd66b6", + "x": 0, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents by Type", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["incident_type.name", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + }, + "sort": { "by": [["count", "desc"]], "top": 10 } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "incident_type.name_uniqueValues", + "yAxisData": ["count"], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": true, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { "type": "auto" } + } + } + } + } + }, + { + "i": "cc30e099-0702-4b7e-95c4-21f3ba5074cc", + "x": 2, + "y": 7, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Schedules", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[schedules]}}", + "name": "schedules", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "timezone", "members", "permalink"], + "hiddenColumns": ["id", "rotationCount", "created_at", "updated_at"] + } + } + } + } + }, + { + "i": "e83e942a-8eeb-41f6-846b-5933eb12b4b9", + "x": 0, + "y": 10, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Incidents", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[incidents]}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["created_at", "desc"]], "top": 15 } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "incident_status.name", + "severity.name", + "incident_type.name", + "created_at", + "permalink" + ], + "hiddenColumns": ["id", "reference", "mode", "visibility", "updated_at"], + "columnDisplayNames": { + "incident_status.name": "Status", + "severity.name": "Severity", + "incident_type.name": "Type", + "created_at": "Created At" + } + } + } + } + } + }, + { + "i": "249f6989-d623-4686-b84f-ff81290e5f4b", + "x": 0, + "y": 13, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Alerts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[alerts]}}", + "name": "alerts", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["created_at", "desc"]], "top": 10 } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["title", "status", "created_at", "resolved_at", "source_url"], + "hiddenColumns": ["id", "alert_source_id", "deduplication_key", "updated_at"], + "columnDisplayNames": { + "created_at": "Created At", + "resolved_at": "Resolved At", + "source_url": "Source URL" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/IncidentIO/v1/defaultContent/schedulePerspective.dash.json b/plugins/IncidentIO/v1/defaultContent/schedulePerspective.dash.json new file mode 100644 index 00000000..814da992 --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/schedulePerspective.dash.json @@ -0,0 +1,101 @@ +{ + "name": "Schedule", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Schedule]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "2f589ed0-e52e-4a53-9445-10ba46af5272", + "x": 0, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Current On-Call", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[scheduleOnCall]}}", + "name": "scheduleOnCall", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Schedules]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Schedule]}}" + }, + "variables": ["{{variables.[Schedule]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["user.name", "user.email", "start_at", "end_at"], + "hiddenColumns": ["rotation_id", "layer_id", "entry_id", "fingerprint"], + "columnDisplayNames": { + "user.name": "On-Call User", + "user.email": "Email", + "start_at": "Start", + "end_at": "End" + } + } + } + } + } + }, + { + "i": "eb35ef1f-62d5-42b4-9b21-3a211155f5d8", + "x": 2, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Shifts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[scheduleShifts]}}", + "name": "scheduleShifts", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["start_at", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Schedules]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Schedule]}}" + }, + "variables": ["{{variables.[Schedule]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["user.name", "user.email", "start_at", "end_at"], + "hiddenColumns": ["rotation_id", "layer_id", "entry_id", "fingerprint"], + "columnDisplayNames": { + "user.name": "On-Call User", + "user.email": "Email", + "start_at": "Start", + "end_at": "End" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/IncidentIO/v1/defaultContent/scopes.json b/plugins/IncidentIO/v1/defaultContent/scopes.json new file mode 100644 index 00000000..e7ca3f78 --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/scopes.json @@ -0,0 +1,38 @@ +[ + { + "name": "Schedules", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Schedule"] } + }, + "variable": { + "name": "Schedule", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Teams", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Team"] } + }, + "variable": { + "name": "Team", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Users", + "matches": { + "sourceType": { "type": "oneOf", "values": ["User"] } + }, + "variable": { + "name": "User", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/IncidentIO/v1/defaultContent/teamPerspective.dash.json b/plugins/IncidentIO/v1/defaultContent/teamPerspective.dash.json new file mode 100644 index 00000000..48f28a5e --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/teamPerspective.dash.json @@ -0,0 +1,46 @@ +{ + "name": "Team", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "3554c41a-1268-4ac9-bf7b-8472314134c8", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "variables": ["{{variables.[Team]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + } + ] + } +} diff --git a/plugins/IncidentIO/v1/defaultContent/userPerspective.dash.json b/plugins/IncidentIO/v1/defaultContent/userPerspective.dash.json new file mode 100644 index 00000000..f7545298 --- /dev/null +++ b/plugins/IncidentIO/v1/defaultContent/userPerspective.dash.json @@ -0,0 +1,90 @@ +{ + "name": "User", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[User]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "6deb863b-e900-4f8a-8064-04ddc6e96f12", + "x": 0, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Users]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[User]}}" + }, + "variables": ["{{variables.[User]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { "transpose": true } + } + } + } + }, + { + "i": "5665353e-5263-4a1d-91d5-d275bb167341", + "x": 2, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Schedules", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[userSchedules]}}", + "name": "userSchedules", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Users]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[User]}}" + }, + "variables": ["{{variables.[User]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["scheduleName", "start_at", "end_at", "schedulePermalink"], + "hiddenColumns": ["rotation_id", "layer_id"], + "columnDisplayNames": { + "scheduleName": "Schedule", + "start_at": "Start", + "end_at": "End", + "schedulePermalink": "Link" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/IncidentIO/v1/docs/README.md b/plugins/IncidentIO/v1/docs/README.md new file mode 100644 index 00000000..b5a9f592 --- /dev/null +++ b/plugins/IncidentIO/v1/docs/README.md @@ -0,0 +1,56 @@ +Monitor your [incident.io](https://incident.io) incidents, alerts, on-call schedules, teams and users in SquaredUp, via the [incident.io API](https://docs.incident.io/api-reference/introduction). + +## Setup + +You will need an incident.io **API key**. + +1. Sign in to your [incident.io dashboard](https://app.incident.io). +2. Go to **Settings → API keys** and click **Create API key**. Give it at least the **View data** permission — the plugin never creates, updates, or deletes anything in incident.io. +3. Copy the generated key and paste it into the **API key** field below. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ----------------- | -------- | +| **API key** | Authenticates every request via the `Authorization: Bearer` header. | incident.io dashboard → **Settings → API keys**. | Yes | + +On save, the plugin validates the key by calling the identity endpoint; an invalid or expired key fails setup with an authentication error. + +## What this plugin monitors + +- **Incidents** — live status, severity and type breakdowns, open/total counts, and a recent-incidents table. +- **Alerts** — live status and a recent-alerts table, including how many are currently firing. +- **On-call schedules** — who's on call right now, recent shifts, and which schedules a given user belongs to. +- **Teams and users** — team membership counts, and each user's role and on-call/response seat access. + +The out-of-the-box dashboards include an estate-wide **Overview** plus a perspective for each of **Schedule**, **Team**, and **User**. + +## Data streams + +- **Current User** — identity and role for the configured API key (used to validate setup; hidden from the tile editor). +- **Incidents** — all incidents in the organization within the selected timeframe, one row per incident. +- **Alerts** — all alerts ingested by your alert sources, one row per alert. +- **Schedule On-Call** — the current on-call shift(s) for one schedule. +- **Schedule Shifts** — the effective shift schedule for one schedule, over a timeframe. +- **User Schedules** — the on-call schedules a given user currently appears in. + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Schedule** | `GET /v2/schedules` | An on-call rotation schedule. | +| **Team** | `GET /v3/teams` | A team, with its member count. | +| **User** | `GET /v2/users` | A person in your incident.io organization. | + +**Incidents and Alerts are not indexed as objects.** They're created and resolved far faster than the platform's default ~12-hour reindex interval — indexing them would mean a new incident could be invisible for hours. Instead, they're served live via the **Incidents** and **Alerts** data streams (always fresh, no reindex lag) and shown as filterable/groupable tiles on the Overview dashboard, rather than as individual drilldown pages. + +## Known limitations + +- **Alerts cap page size at 50 records** (vs 100+ for schedules/teams/users); large alert volumes are paged automatically but may take longer to query. +- **Alert data depends on your incident.io setup** — if no alert sources are configured, the Alerts stream and its Overview tiles will simply be empty rather than erroring. +- **No per-incident or per-alert drilldown dashboard** — since neither is indexed as an object (see above), there's no dedicated page for a single incident's update timeline, attached alerts, or escalations; use the incident.io web app for that level of detail. +- **Schedule shifts show recent history, not future shifts** — the Schedule perspective's shift tile uses the standard dashboard timeframe (which only looks backward), so upcoming on-call shifts aren't shown; check the incident.io on-call UI for future rotations. +- **Teams requires the `catalog_entries.view` scope** on your API key — without it, team import and the Team object type will be unavailable. +- **User Schedules checks only the first page of schedules** — for organizations with more than ~100 schedules, a user's on-call status on schedules beyond the first page won't be reflected. +- **Rate limits** — the incident.io API allows up to 1,200 requests/minute per key, with a lower 60/minute limit specifically on listing incidents; very large estates may query more slowly as a result. +- **Read-only** — the plugin never creates, modifies, or deletes anything in incident.io. diff --git a/plugins/IncidentIO/v1/icon.svg b/plugins/IncidentIO/v1/icon.svg new file mode 100644 index 00000000..e43a9526 --- /dev/null +++ b/plugins/IncidentIO/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/IncidentIO/v1/indexDefinitions/default.json b/plugins/IncidentIO/v1/indexDefinitions/default.json new file mode 100644 index 00000000..9830bab2 --- /dev/null +++ b/plugins/IncidentIO/v1/indexDefinitions/default.json @@ -0,0 +1,44 @@ +{ + "steps": [ + { + "name": "schedules", + "dataStream": { "name": "schedules" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Schedule" }, + "properties": ["timezone", "created_at", "updated_at", "permalink"] + } + }, + { + "name": "teams", + "dataStream": { "name": "teams" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Team" }, + "properties": ["memberCount"] + } + }, + { + "name": "users", + "dataStream": { "name": "users" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "User" }, + "properties": [ + "email", + "role", + "is_active", + "slack_user_id", + { "onCallSeat": "seats.on_call" }, + { "responseSeat": "seats.response" } + ] + } + } + ] +} diff --git a/plugins/IncidentIO/v1/metadata.json b/plugins/IncidentIO/v1/metadata.json new file mode 100644 index 00000000..7d806ceb --- /dev/null +++ b/plugins/IncidentIO/v1/metadata.json @@ -0,0 +1,36 @@ +{ + "name": "incident-io", + "displayName": "incident.io", + "version": "1.0.0", + "author": { "name": "noorul-khan", "type": "community" }, + "description": "Monitor incidents, alerts, on-call schedules, teams and users from incident.io in SquaredUp.", + "category": "Alert Management", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["incident management", "on-call", "alerting", "incident response"], + "objectTypes": ["Schedule", "Team", "User"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/IncidentIO/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/IncidentIO/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.incident.io/", + "authMode": "none", + "headers": [{ "key": "Authorization", "value": "Bearer {{apiKey}}" }], + "queryArgs": [] + } + } +} diff --git a/plugins/IncidentIO/v1/ui.json b/plugins/IncidentIO/v1/ui.json new file mode 100644 index 00000000..a8b552ca --- /dev/null +++ b/plugins/IncidentIO/v1/ui.json @@ -0,0 +1,10 @@ +[ + { + "type": "password", + "name": "apiKey", + "label": "API key", + "help": "Create an API key in incident.io under **Settings → API keys** with at least the **View data** permission.", + "placeholder": "inc_xxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "validation": { "required": true } + } +]