diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx index 5f489e0c7a84b..a19b9f3d378ed 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx @@ -45,6 +45,16 @@ export function DashboardMissingData() { state.statsAttempt.data.awseks = undefined; state.statsAttempt.data.awsrds = undefined; state.statsAttempt.data.awsec2 = undefined; + state.tasksAttempt.data.items = [ + { + name: '', + taskType: 'ec2', + state: '', + issueType: 'lost-connection', + integration: '', + lastStateChange: '', + }, + ]; return ( @@ -174,6 +184,11 @@ function makeAwsOidcStatusContextState( awseks: makeResourceTypeSummary(), }, }, + tasksAttempt: { + status: 'success', + statusText: '', + data: {}, + }, }, overrides ); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx index a18c9d5e824de..33a2bd1d60c92 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx @@ -28,9 +28,10 @@ import { StatCard, } from 'teleport/Integrations/status/AwsOidc/StatCard'; import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; +import { TaskAlert } from 'teleport/Integrations/status/AwsOidc/Tasks/TaskAlert'; export function AwsOidcDashboard() { - const { statsAttempt, integrationAttempt } = useAwsOidcStatus(); + const { statsAttempt, integrationAttempt, tasksAttempt } = useAwsOidcStatus(); if ( statsAttempt.status == 'processing' || @@ -50,11 +51,15 @@ export function AwsOidcDashboard() { // todo (michellescripts) after routing, ensure this view can be sticky const { awsec2, awseks, awsrds } = statsAttempt.data; const { data: integration } = integrationAttempt; + const { data: tasks } = tasksAttempt; return ( <> - + {integration && } + {tasks && tasks.items.length > 0 && ( + + )}

Auto-Enrollment

- {!resource ? ( + {!resource && !tasks ? ( <> {divider} @@ -85,12 +87,24 @@ export function AwsOidcHeader({ > {integration.name} + + )} + {resource && ( + <> {divider} {resource.toUpperCase()} )} + {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..60dc8b7a5e29a 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} /> + @@ -60,3 +55,31 @@ export function AwsOidcTitle({ ); } + +function getContent( + integration: Integration, + resource?: AwsResource, + tasks?: boolean +): { to: string; helper: string; content: string } { + if (resource) { + return { + to: cfg.getIntegrationStatusRoute(integration.kind, integration.name), + helper: 'Back to integration', + content: resource.toUpperCase(), + }; + } + + if (tasks) { + return { + to: cfg.getIntegrationStatusRoute(integration.kind, integration.name), + helper: 'Back to integration', + content: 'Pending Tasks', + }; + } + + return { + to: cfg.routes.integrations, + helper: 'Back to integrations', + content: integration.name, + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx index 02d185412509e..97defcd02416f 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx @@ -26,6 +26,7 @@ import { IntegrationKind } from 'teleport/services/integrations'; import { Rds } from 'teleport/Integrations/status/AwsOidc/Details/Rds'; import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; +import { TaskAlert } from 'teleport/Integrations/status/AwsOidc/Tasks/TaskAlert'; export function Details() { const { resourceKind } = useParams<{ @@ -34,17 +35,23 @@ export function Details() { resourceKind: AwsResource; }>(); - const { integrationAttempt } = useAwsOidcStatus(); + const { integrationAttempt, tasksAttempt } = useAwsOidcStatus(); const { data: integration } = integrationAttempt; + const { data: tasks } = tasksAttempt; return ( <> {integration && ( )} - + + <> {integration && ( )} + {tasks && tasks.items.length > 0 && ( + + )} + {resourceKind == AwsResource.ec2 && } {resourceKind == AwsResource.eks && } {resourceKind == AwsResource.rds && } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx new file mode 100644 index 0000000000000..123472fa3bfe7 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx @@ -0,0 +1,37 @@ +import { Alert } from 'design'; +import { ArrowForward, BellRinging } from 'design/Icon'; +import cfg from 'teleport/config'; +import { IntegrationKind } from 'teleport/services/integrations'; +import { useHistory } from 'react-router'; + +type TaskAlertProps = { + name: string; + total: number; + kind?: IntegrationKind; +}; + +export function TaskAlert({ + name, + total, + kind = IntegrationKind.AwsOidc, +}: TaskAlertProps) { + const history = useHistory(); + + return ( + + Resolve Now + + + ), + onClick: () => history.push(cfg.getIntegrationTasksRoute(kind, name)), + }} + > + {total} Pending Tasks + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx new file mode 100644 index 0000000000000..2292b12b26406 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx @@ -0,0 +1,90 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Table, { Cell } from 'design/DataTable'; + +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 { UserTask } from 'teleport/services/integrations'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; + +export function Tasks() { + const { integrationAttempt, tasksAttempt } = useAwsOidcStatus(); + const { data: integration } = integrationAttempt; + const { data: tasks } = tasksAttempt; + + if ( + integrationAttempt.status == 'processing' || + tasksAttempt.status == 'processing' + ) { + return ; + } + + if (integrationAttempt.status == 'error' || tasksAttempt.status == 'error') { + return ( + + {integrationAttempt.status == 'error' + ? integrationAttempt.statusText + : tasksAttempt.statusText} + + ); + } + + if (!tasks) { + return null; + } + + return ( + <> + {integration && } + + {integration && } + + data={tasks.items} + columns={[ + { + key: 'taskType', + headerText: 'Type', + isSortable: true, + }, + { + key: 'issueType', + headerText: 'Issue Details', + isSortable: true, + }, + { + key: 'lastStateChange', + headerText: 'Timestamp (UTC)', + isSortable: true, + render: (item: UserTask) => ( + {new Date(item.lastStateChange).toISOString()} + ), + }, + ]} + emptyText={`No pending tasks`} + isSearchable + /> + + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx index c49d97d7ba577..c9325b3cb6b6f 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx @@ -25,6 +25,7 @@ import { IntegrationKind, integrationService, IntegrationWithSummary, + UserTasksListResponse, } from 'teleport/services/integrations'; import useTeleport from 'teleport/useTeleport'; @@ -32,6 +33,7 @@ import useTeleport from 'teleport/useTeleport'; export interface AwsOidcStatusContextState { statsAttempt: Attempt; integrationAttempt: Attempt; + tasksAttempt: Attempt; } export const awsOidcStatusContext = @@ -54,16 +56,22 @@ export function AwsOidcStatusProvider({ children }: React.PropsWithChildren) { integrationService.fetchIntegration(name) ); + const [tasks, fetchTasks] = useAsync(() => + integrationService.fetchIntegrationUserTasksList(name) + ); + useEffect(() => { if (hasIntegrationReadAccess) { fetchIntegrationStats(); fetchIntegration(); + fetchTasks(); } }, []); const value: AwsOidcStatusContextState = { statsAttempt: stats, integrationAttempt: integration, + tasksAttempt: tasks, }; return ( diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 53c3cea31a766..e3437d1d56204 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: @@ -562,6 +565,10 @@ const cfg = { }); }, + getIntegrationTasksRoute(type: PluginKind | IntegrationKind, name: string) { + return generatePath(cfg.routes.integrationTasks, { type, name }); + }, + getMsTeamsAppZipRoute(clusterId: string, plugin: string) { return generatePath(cfg.api.msTeamsAppZipPath, { clusterId, plugin }); }, @@ -1026,6 +1033,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..0c04780ca15f3 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..36862b0356585 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,15 @@ 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..29edca253082a 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -306,6 +306,33 @@ 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; + // lastStateChange indicates when the current's user task state was last changed. + lastStateChange: string; +}; + // IntegrationDiscoveryRule describes a discovery rule associated with an integration. export type IntegrationDiscoveryRule = { // resourceType indicates the type of resource that this rule targets.