From c2292b0a3a80aa7ea6b0e25c1d907c406ea65653 Mon Sep 17 00:00:00 2001 From: Michelle Bergquist Date: Tue, 24 Dec 2024 15:13:54 -0700 Subject: [PATCH] Add user tasks tables for aws resources --- .../status/AwsOidc/AwsOidcHeader.tsx | 10 +++ .../status/AwsOidc/AwsOidcRoutes.tsx | 7 ++ .../status/AwsOidc/Tasks/Tasks.tsx | 88 +++++++++++++++++++ web/packages/teleport/src/config.ts | 11 +++ .../integrations/integrations.test.ts | 42 +++++++++ .../src/services/integrations/integrations.ts | 14 +++ .../src/services/integrations/types.ts | 25 ++++++ 7 files changed, 197 insertions(+) create mode 100644 web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx index 83beb1314bd92..c7072f601ab5a 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx @@ -30,9 +30,11 @@ import { useHistory } from 'react-router'; export function AwsOidcHeader({ integration, resource = undefined, + tasks = false, }: { integration: Integration; resource?: AwsResource; + tasks?: boolean; }) { const history = useHistory(); const divider = ( @@ -90,6 +92,14 @@ export function AwsOidcHeader({ )} + {tasks && ( + <> + {divider} + + Pending Tasks + + + )} ); } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx index 50d154d378e85..a04bf5f2947ff 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx @@ -22,6 +22,7 @@ import cfg from 'teleport/config'; import { AwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; import { Details } from 'teleport/Integrations/status/AwsOidc/Details/Details'; +import {Tasks} from "teleport/Integrations/status/AwsOidc/Tasks/Tasks"; import { AwsOidcDashboard } from './AwsOidcDashboard'; @@ -35,6 +36,12 @@ export function AwsOidcRoutes() { path={cfg.routes.integrationStatusResources} component={Details} /> + . + */ + +import React, {useEffect} from 'react'; +import Table from 'design/DataTable'; + +import {useAsync} from "shared/hooks/useAsync"; + +import {useParams} from "react-router"; + +import {Indicator} from "design"; + +import {Danger} from "design/Alert"; + +import { FeatureBox } from 'teleport/components/Layout'; +import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader'; +import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import {IntegrationKind, integrationService, UserTask} from "teleport/services/integrations"; + + +export function Tasks() { + const { name } = useParams<{ + type: IntegrationKind; + name: string; + }>(); + + const { integrationAttempt } = useAwsOidcStatus(); + const { data: integration } = integrationAttempt; + + const [attempt, fetchTasks] = useAsync(() => + integrationService.fetchIntegrationUserTasksList(name) + ); + + useEffect(() => { + fetchTasks(); + }, []); + + if (attempt.status == 'processing') { + return ; + } + + if (attempt.status == 'error') { + return {attempt.statusText}; + } + + if (!attempt.data) { + return null; + } + + return ( + + {integration && } + {/* todo (michellescripts) sync with Marco on timestamp field */} + + data={attempt.data.items} + columns={[ + { + key: 'taskType', + headerText: 'Type', + isSortable: true, + }, + { + key: 'name', + headerText: 'Issue Details', + isSortable: true, + }, + ]} + emptyText={`No pending tasks`} + isSearchable + /> + + ); +} diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index bc08ea147e425..1f6424ca9c795 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -199,6 +199,7 @@ const cfg = { headlessSso: `/web/headless/:requestId`, integrations: '/web/integrations', integrationStatus: '/web/integrations/status/:type/:name', + integrationTasks: '/web/integrations/status/:type/:name/tasks', integrationStatusResources: '/web/integrations/status/:type/:name/resources/:resourceKind', integrationEnroll: '/web/integrations/new/:type?', @@ -334,6 +335,8 @@ const cfg = { '/v1/webapi/sites/:clusterId/integrations/:name/stats', integrationRulesPath: '/v1/webapi/sites/:clusterId/integrations/:name/discoveryrules?resourceType=:resourceType?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?&limit=:limit?', + userTaskListByIntegrationPath: + '/v1/webapi/sites/:clusterId/usertask?integration=:name', thumbprintPath: '/v1/webapi/thumbprint', pingAwsOidcIntegrationPath: @@ -1026,6 +1029,14 @@ const cfg = { }); }, + getIntegrationUserTasksListUrl(name: string) { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.userTaskListByIntegrationPath, { + clusterId, + name, + }); + }, + getPingAwsOidcIntegrationUrl({ integrationName, clusterId, diff --git a/web/packages/teleport/src/services/integrations/integrations.test.ts b/web/packages/teleport/src/services/integrations/integrations.test.ts index a799dd90246c8..68637fa311120 100644 --- a/web/packages/teleport/src/services/integrations/integrations.test.ts +++ b/web/packages/teleport/src/services/integrations/integrations.test.ts @@ -276,6 +276,48 @@ test('fetch integration rules: fetchIntegrationRules()', async () => { }); }); +test('fetch integration user task list: fetchIntegrationUserTasksList()', async () => { + // test a valid response + jest.spyOn(api, 'get').mockResolvedValue({ + items: [ + { + name: "task-name", + taskType: "task-type", + state: "task-state", + issueType: "issue-type", + integration: "name", + }, + ], + nextKey: 'some-key', + }); + + let response = await integrationService.fetchIntegrationUserTasksList('name'); + expect(api.get).toHaveBeenCalledWith( + cfg.getIntegrationUserTasksListUrl('name') + ); + expect(response).toEqual({ + nextKey: 'some-key', + items: [ + { + name: "task-name", + taskType: "task-type", + state: "task-state", + issueType: "issue-type", + integration: "name", + }, + ], + }); + + // test null response + jest.spyOn(api, 'get').mockResolvedValue(null); + + response = await integrationService.fetchIntegrationUserTasksList('name'); + expect(response).toEqual({ + nextKey: undefined, + items: [], + }); +}); + const nonAwsOidcIntegration = { name: 'non-aws-oidc-integration', subKind: 'abc', diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index f7fb6db4f4b2b..646418cc2e8a1 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -63,6 +63,7 @@ import { AwsOidcPingRequest, IntegrationWithSummary, IntegrationDiscoveryRules, + UserTasksListResponse, } from './types'; export const integrationService = { @@ -446,6 +447,19 @@ export const integrationService = { }; }); }, + + fetchIntegrationUserTasksList( + name: string, + ): Promise { + return api + .get(cfg.getIntegrationUserTasksListUrl(name)) + .then(resp => { + return { + items: resp?.items || [], + nextKey: resp?.nextKey, + }; + }); + }, }; export function makeIntegrations(json: any): Integration[] { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index d63cac5c85a96..4c1e49c8b0b39 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -306,6 +306,31 @@ export type IntegrationDiscoveryRules = { nextKey: string; }; +// UserTasksListResponse contains a list of UserTasks. +// In case of exceeding the pagination limit (either via query param `limit` or the default 1000) +// a `nextToken` is provided and should be used to obtain the next page (as a query param `startKey`) +export type UserTasksListResponse = { + // items is a list of resources retrieved. + items: UserTask[]; + // nextKey is the position to resume listing events. + nextKey: string; +}; + +// UserTask describes UserTask fields. +// Used for listing User Tasks without receiving all the details. +export type UserTask = { + // name is the UserTask name. + name: string; + // taskType identifies this task's type. + taskType: string; + // state is the state for the User Task. + state: string; + // issueType identifies this task's issue type. + issueType: string; + // integration is the Integration Name this User Task refers to. + integration: string; +}; + // IntegrationDiscoveryRule describes a discovery rule associated with an integration. export type IntegrationDiscoveryRule = { // resourceType indicates the type of resource that this rule targets.