diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
index d52c123d7a105..607ec652f13cf 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 = (
@@ -62,7 +64,7 @@ export function AwsOidcHeader({
- {!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}
/>
+
.
+ */
+
+import { useEffect } from 'react';
+import Table, { Cell } 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 && }
+
+ data={attempt.data.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/config.ts b/web/packages/teleport/src/config.ts
index 53c3cea31a766..4dc42b9aa593f 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..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.