diff --git a/web/packages/design/src/Tabs/Tabs.ts b/web/packages/design/src/Tabs/Tabs.ts
new file mode 100644
index 0000000000000..b8e7a3dd15826
--- /dev/null
+++ b/web/packages/design/src/Tabs/Tabs.ts
@@ -0,0 +1,58 @@
+/**
+ * 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 styled from 'styled-components';
+
+import { NavLink } from 'react-router-dom';
+
+export const TabsContainer = styled.div`
+ position: relative;
+ display: flex;
+ gap: ${p => p.theme.space[5]}px;
+ align-items: center;
+ padding: 0 ${p => p.theme.space[5]}px;
+ border-bottom: 1px solid ${p => p.theme.colors.spotBackground[0]};
+`;
+
+export const TabContainer = styled(NavLink)<{ selected?: boolean }>`
+ padding: ${p => p.theme.space[1] + p.theme.space[2]}px
+ ${p => p.theme.space[2]}px;
+ position: relative;
+ cursor: pointer;
+ z-index: 2;
+ opacity: ${p => (p.selected ? 1 : 0.5)};
+ transition: opacity 0.3s linear;
+ color: ${p => p.theme.colors.text.main};
+ font-weight: 300;
+ font-size: 22px;
+ line-height: ${p => p.theme.space[5]}px;
+ white-space: nowrap;
+ text-decoration: none;
+
+ &:hover {
+ opacity: 1;
+ }
+`;
+
+export const TabBorder = styled.div`
+ position: absolute;
+ bottom: -1px;
+ background: ${p => p.theme.colors.brand};
+ height: 2px;
+ transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
+`;
diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx
index a7eadd708a7e1..77044cf0076bc 100644
--- a/web/packages/teleport/src/Integrations/IntegrationList.tsx
+++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router';
import { Link as InternalRouteLink } from 'react-router-dom';
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx
index 1f20dfa40f9d5..a18c9d5e824de 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx
@@ -27,17 +27,23 @@ import {
AwsResource,
StatCard,
} from 'teleport/Integrations/status/AwsOidc/StatCard';
+import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle';
export function AwsOidcDashboard() {
const { statsAttempt, integrationAttempt } = useAwsOidcStatus();
- if (statsAttempt.status == 'processing') {
+ if (
+ statsAttempt.status == 'processing' ||
+ integrationAttempt.status == 'processing'
+ ) {
return ;
}
- if (statsAttempt.status == 'error') {
+
+ if (integrationAttempt.status == 'error') {
return {statsAttempt.statusText};
}
- if (!statsAttempt.data) {
+
+ if (!statsAttempt.data || !integrationAttempt.data) {
return null;
}
@@ -45,14 +51,29 @@ export function AwsOidcDashboard() {
const { awsec2, awseks, awsrds } = statsAttempt.data;
const { data: integration } = integrationAttempt;
return (
-
- {integration && }
-
Auto-Enrollment
-
-
-
-
-
-
+ <>
+
+
+ {integration && }
+
Auto-Enrollment
+
+
+
+
+
+
+ >
);
}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
index c000977305e08..83beb1314bd92 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
@@ -18,33 +18,78 @@
import { Link as InternalLink } from 'react-router-dom';
-import { ButtonIcon, Flex, Label, Text } from 'design';
-import { ArrowLeft } from 'design/Icon';
+import { ButtonIcon, ButtonText, Flex, Text } from 'design';
+import { Plugs } from 'design/Icon';
import { HoverTooltip } from 'design/Tooltip';
import cfg from 'teleport/config';
-import { getStatusAndLabel } from 'teleport/Integrations/helpers';
import { Integration } from 'teleport/services/integrations';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+import { useHistory } from 'react-router';
+
+export function AwsOidcHeader({
+ integration,
+ resource = undefined,
+}: {
+ integration: Integration;
+ resource?: AwsResource;
+}) {
+ const history = useHistory();
+ const divider = (
+
+ /
+
+ );
-export function AwsOidcHeader({ integration }: { integration: Integration }) {
- const { status, labelKind } = getStatusAndLabel(integration);
return (
-
+
-
+
-
- {integration.name}
-
-
+ {!resource ? (
+ <>
+ {divider}
+
+ {integration.name}
+
+ >
+ ) : (
+ <>
+ {divider}
+
+ history.push(
+ cfg.getIntegrationStatusRoute(
+ integration.kind,
+ integration.name
+ )
+ )
+ }
+ >
+ {integration.name}
+
+ {divider}
+
+ {resource.toUpperCase()}
+
+ >
+ )}
);
}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx
index bd69b287af243..50d154d378e85 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx
@@ -21,6 +21,8 @@ import cfg from 'teleport/config';
import { AwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus';
+import { Details } from 'teleport/Integrations/status/AwsOidc/Details/Details';
+
import { AwsOidcDashboard } from './AwsOidcDashboard';
export function AwsOidcRoutes() {
@@ -28,7 +30,13 @@ export function AwsOidcRoutes() {
+ .
+ */
+
+import { Link as InternalLink } from 'react-router-dom';
+
+import { ButtonIcon, Flex, Label, Text } from 'design';
+import { ArrowLeft } from 'design/Icon';
+import { HoverTooltip } from 'design/Tooltip';
+
+import cfg from 'teleport/config';
+import { getStatusAndLabel } from 'teleport/Integrations/helpers';
+import { Integration } from 'teleport/services/integrations';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+
+export function AwsOidcTitle({
+ integration,
+ resource = undefined,
+}: {
+ integration: Integration;
+ resource?: AwsResource;
+}) {
+ const { status, labelKind } = getStatusAndLabel(integration);
+
+ const content = {
+ to: !resource
+ ? cfg.routes.integrations
+ : cfg.getIntegrationStatusRoute(integration.kind, integration.name),
+ helper: !resource ? 'Back to integrations' : 'Back to integration',
+ content: !resource ? integration.name : resource.toUpperCase(),
+ };
+
+ return (
+
+
+
+
+
+
+
+ {content.content}
+
+
+
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx
new file mode 100644
index 0000000000000..dae7b472525e1
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx
@@ -0,0 +1,60 @@
+/**
+ * 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, { LabelCell } from 'design/DataTable';
+
+export function Agents() {
+ return (
+
{
+ const aStr = a.tags.toString();
+ const bStr = b.tags.toString();
+
+ if (aStr < bStr) {
+ return -1;
+ }
+ if (aStr > bStr) {
+ return 1;
+ }
+
+ return 0;
+ },
+ render: ({ tags }) => ,
+ },
+ ]}
+ emptyText="Agents details coming soon"
+ isSearchable
+ />
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx
new file mode 100644
index 0000000000000..02d185412509e
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx
@@ -0,0 +1,54 @@
+/**
+ * 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 { useParams } from 'react-router';
+
+import { FeatureBox } from 'teleport/components/Layout';
+import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader';
+import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+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';
+
+export function Details() {
+ const { resourceKind } = useParams<{
+ type: IntegrationKind;
+ name: string;
+ resourceKind: AwsResource;
+ }>();
+
+ const { integrationAttempt } = useAwsOidcStatus();
+ const { data: integration } = integrationAttempt;
+ return (
+ <>
+ {integration && (
+
+ )}
+
+ {integration && (
+
+ )}
+ {resourceKind == AwsResource.ec2 && }
+ {resourceKind == AwsResource.eks && }
+ {resourceKind == AwsResource.rds && }
+
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx
new file mode 100644
index 0000000000000..bb4d8f6973bda
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx
@@ -0,0 +1,101 @@
+/**
+ * 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 { useEffect, useRef } from 'react';
+
+import { TabBorder, TabContainer, TabsContainer } from 'design/Tabs/Tabs';
+import { useLocation, useParams } from 'react-router';
+
+import { IntegrationKind } from 'teleport/services/integrations';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+import cfg from 'teleport/config';
+import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules';
+import { Agents } from 'teleport/Integrations/status/AwsOidc/Details/Agents';
+
+export enum RdsTab {
+ Agents = 'agents',
+ Rules = 'rules',
+}
+
+export function Rds() {
+ const { type, name, resourceKind } = useParams<{
+ type: IntegrationKind;
+ name: string;
+ resourceKind: AwsResource;
+ }>();
+
+ const { search } = useLocation();
+ const searchParams = new URLSearchParams(search);
+ const tab = (searchParams.get('tab') as RdsTab) || RdsTab.Rules;
+
+ const borderRef = useRef(null);
+ const parentRef = useRef();
+
+ useEffect(() => {
+ if (!parentRef.current || !borderRef.current) {
+ return;
+ }
+
+ const activeElement = parentRef.current.querySelector(
+ `[data-tab-id="${tab}"]`
+ );
+
+ if (activeElement) {
+ const parentBounds = parentRef.current.getBoundingClientRect();
+ const activeBounds = activeElement.getBoundingClientRect();
+
+ const left = activeBounds.left - parentBounds.left;
+ const width = activeBounds.width;
+
+ borderRef.current.style.left = `${left}px`;
+ borderRef.current.style.width = `${width}px`;
+ }
+ }, [tab]);
+
+ return (
+ <>
+
+
+ Enrollment Rules
+
+
+ Agents
+
+
+
+ {tab == RdsTab.Rules && }
+ {tab == RdsTab.Agents && }
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx
new file mode 100644
index 0000000000000..0a529848c31b9
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx
@@ -0,0 +1,110 @@
+/**
+ * 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 { render, screen, waitFor } from 'design/utils/testing';
+
+import { MemoryRouter } from 'react-router';
+
+import { within } from '@testing-library/react';
+
+import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules';
+import {
+ IntegrationDiscoveryRule,
+ integrationService,
+} from 'teleport/services/integrations';
+
+test('renders region & labels from response', async () => {
+ jest.spyOn(integrationService, 'fetchIntegrationRules').mockResolvedValue({
+ rules: [
+ makeIntegrationDiscoveryRule({
+ region: 'us-west-2',
+ labelMatcher: [
+ { name: 'env', value: 'prod' },
+ { name: 'key', value: '123' },
+ ],
+ }),
+ makeIntegrationDiscoveryRule({
+ region: 'us-east-2',
+ labelMatcher: [{ name: 'env', value: 'stage' }],
+ }),
+ makeIntegrationDiscoveryRule({
+ region: 'us-west-1',
+ labelMatcher: [{ name: 'env', value: 'test' }],
+ }),
+ makeIntegrationDiscoveryRule({
+ region: 'us-east-1',
+ labelMatcher: [{ name: 'env', value: 'dev' }],
+ }),
+ ],
+ nextKey: '',
+ });
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('env:prod')).toBeInTheDocument();
+ });
+
+ expect(getTableCellContents()).toEqual({
+ header: ['Region', 'Labels'],
+ rows: [
+ ['us-west-2', 'env:prodkey:123'],
+ ['us-east-2', 'env:stage'],
+ ['us-west-1', 'env:test'],
+ ['us-east-1', 'env:dev'],
+ ],
+ });
+
+ jest.clearAllMocks();
+});
+
+function makeIntegrationDiscoveryRule(
+ overrides: Partial = {}
+): IntegrationDiscoveryRule {
+ return Object.assign(
+ {
+ resourceType: '',
+ region: '',
+ labelMatcher: [],
+ discoveryConfig: '',
+ lastSync: 0,
+ },
+ overrides
+ );
+}
+
+function getTableCellContents() {
+ const [header, ...rows] = screen.getAllByRole('row');
+ return {
+ header: within(header)
+ .getAllByRole('columnheader')
+ .map(cell => cell.textContent),
+ rows: rows.map(row =>
+ within(row)
+ .getAllByRole('cell')
+ .map(cell => cell.textContent)
+ ),
+ };
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx
new file mode 100644
index 0000000000000..2b0539f22f6bd
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx
@@ -0,0 +1,129 @@
+/**
+ * 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 { useEffect, useState } from 'react';
+
+import Table, { LabelCell } from 'design/DataTable';
+
+import { useParams } from 'react-router';
+
+import {
+ IntegrationDiscoveryRule,
+ IntegrationKind,
+ integrationService,
+} from 'teleport/services/integrations';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+import { SearchPanel } from 'shared/components/Search';
+import { useServerSidePagination } from 'teleport/components/hooks';
+import { SortType } from 'design/DataTable/types';
+
+export function Rules() {
+ const { name, resourceKind } = useParams<{
+ type: IntegrationKind;
+ name: string;
+ resourceKind: AwsResource;
+ }>();
+
+ const [search, setSearch] = useState('');
+ const [sort, setSort] = useState({
+ fieldName: 'region',
+ dir: 'ASC',
+ });
+ const serverSidePagination =
+ useServerSidePagination({
+ pageSize: 20,
+ fetchFunc: async (_, params) => {
+ const { rules, nextKey } =
+ await integrationService.fetchIntegrationRules(
+ name,
+ resourceKind,
+ params
+ );
+ return { agents: rules, nextKey };
+ },
+ clusterId: '',
+ params: { search, sort },
+ });
+
+ useEffect(() => {
+ serverSidePagination.fetch();
+ }, [search, sort]);
+
+ return (
+
+ data={serverSidePagination.fetchedData.agents || undefined}
+ columns={[
+ {
+ key: 'region',
+ headerText: 'Region',
+ isSortable: true,
+ },
+ {
+ key: 'labelMatcher',
+ headerText: getResourceTerm(resourceKind),
+ isSortable: true,
+ onSort: (a, b) => {
+ const aStr = a.labelMatcher.toString();
+ const bStr = b.labelMatcher.toString();
+
+ if (aStr < bStr) {
+ return -1;
+ }
+ if (aStr > bStr) {
+ return 1;
+ }
+
+ return 0;
+ },
+ render: ({ labelMatcher }) => (
+ `${l.name}:${l.value}`)} />
+ ),
+ },
+ ]}
+ emptyText={`No ${resourceKind} data`}
+ isSearchable
+ fetching={{
+ fetchStatus: serverSidePagination.fetchStatus,
+ onFetchNext: serverSidePagination.fetchNext,
+ onFetchPrev: serverSidePagination.fetchPrev,
+ }}
+ serversideProps={{
+ sort: sort,
+ setSort: setSort,
+ serversideSearchPanel: (
+
+ ),
+ }}
+ />
+ );
+}
+
+function getResourceTerm(resource: AwsResource): string {
+ switch (resource) {
+ case AwsResource.rds:
+ return 'Tags';
+ default:
+ return 'Labels';
+ }
+}
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx
index d219077bb81aa..d967a7ebe1f33 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx
@@ -23,7 +23,15 @@ import * as Icons from 'design/Icon';
import { formatDistanceStrict } from 'date-fns';
-import { ResourceTypeSummary } from 'teleport/services/integrations';
+import styled from 'styled-components';
+
+import history from 'teleport/services/history';
+
+import {
+ IntegrationKind,
+ ResourceTypeSummary,
+} from 'teleport/services/integrations';
+import cfg from 'teleport/config';
export enum AwsResource {
ec2 = 'ec2',
@@ -32,22 +40,29 @@ export enum AwsResource {
}
type StatCardProps = {
+ name: string;
resource: AwsResource;
summary?: ResourceTypeSummary;
};
-export function StatCard({ resource, summary }: StatCardProps) {
+export function StatCard({ name, resource, summary }: StatCardProps) {
const updated = summary?.discoverLastSync
? new Date(summary?.discoverLastSync)
: undefined;
const term = getResourceTerm(resource);
return (
- {
+ history.push(
+ cfg.getIntegrationStatusResourcesRoute(
+ IntegrationKind.AwsOidc,
+ name,
+ resource
+ )
+ );
+ }}
>
)}
-
+
);
}
@@ -111,3 +126,17 @@ function getResourceTerm(resource: AwsResource): string {
return 'Instances';
}
}
+
+export const SelectCard = styled(Card)`
+ width: 33%;
+ background-color: ${props => props.theme.colors.levels.surface};
+ padding: 12px;
+ border-radius: ${props => props.theme.radii[2]}px;
+ border: ${props => `1px solid ${props.theme.colors.levels.surface}`};
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${props => props.theme.colors.levels.elevated};
+ box-shadow: ${({ theme }) => theme.boxShadow[2]};
+ }
+`;
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx
index 4d29af57d0422..d667ccf0bb258 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { MemoryRouter } from 'react-router';
import {
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx
index f517f785595d9..c49d97d7ba577 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { createContext, useContext, useEffect } from 'react';
+import { createContext, useContext, useEffect } from 'react';
import { useParams } from 'react-router';
import { Attempt, useAsync } from 'shared/hooks/useAsync';
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index f475fa868e8ac..bc08ea147e425 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -26,6 +26,7 @@ import {
PluginKind,
Regions,
} from 'teleport/services/integrations';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
import { defaultEntitlements } from './entitlement';
@@ -198,6 +199,8 @@ const cfg = {
headlessSso: `/web/headless/:requestId`,
integrations: '/web/integrations',
integrationStatus: '/web/integrations/status/:type/:name',
+ integrationStatusResources:
+ '/web/integrations/status/:type/:name/resources/:resourceKind',
integrationEnroll: '/web/integrations/new/:type?',
locks: '/web/locks',
newLock: '/web/locks/new',
@@ -329,6 +332,9 @@ const cfg = {
integrationsPath: '/v1/webapi/sites/:clusterId/integrations/:name?',
integrationStatsPath:
'/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?',
+
thumbprintPath: '/v1/webapi/thumbprint',
pingAwsOidcIntegrationPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/ping',
@@ -544,6 +550,18 @@ const cfg = {
return generatePath(cfg.routes.integrationStatus, { type, name });
},
+ getIntegrationStatusResourcesRoute(
+ type: PluginKind | IntegrationKind,
+ name: string,
+ resourceKind: AwsResource
+ ) {
+ return generatePath(cfg.routes.integrationStatusResources, {
+ type,
+ name,
+ resourceKind,
+ });
+ },
+
getMsTeamsAppZipRoute(clusterId: string, plugin: string) {
return generatePath(cfg.api.msTeamsAppZipPath, { clusterId, plugin });
},
@@ -994,6 +1012,20 @@ const cfg = {
});
},
+ getIntegrationRulesUrl(
+ name: string,
+ resourceType: AwsResource,
+ params?: UrlResourcesParams
+ ) {
+ const clusterId = cfg.proxyCluster;
+ return generateResourcePath(cfg.api.integrationRulesPath, {
+ clusterId,
+ name,
+ resourceType,
+ ...params,
+ });
+ },
+
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 fb917c96a6b97..a799dd90246c8 100644
--- a/web/packages/teleport/src/services/integrations/integrations.test.ts
+++ b/web/packages/teleport/src/services/integrations/integrations.test.ts
@@ -19,8 +19,10 @@
import api from 'teleport/services/api';
import cfg from 'teleport/config';
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
+
import { integrationService } from './integrations';
-import { IntegrationStatusCode, IntegrationAudience } from './types';
+import { IntegrationAudience, IntegrationStatusCode } from './types';
test('fetch a single integration: fetchIntegration()', async () => {
// test a valid response
@@ -226,6 +228,54 @@ describe('fetchAwsDatabases() request body formatting', () => {
);
});
+test('fetch integration rules: fetchIntegrationRules()', async () => {
+ // test a valid response
+ jest.spyOn(api, 'get').mockResolvedValue({
+ rules: [
+ {
+ resourceType: 'eks',
+ region: 'us-west-2',
+ labelMatcher: [{ name: 'env', value: 'dev' }],
+ discoveryConfig: 'cfg',
+ lastSync: 1733782634,
+ },
+ ],
+ nextKey: 'some-key',
+ });
+
+ let response = await integrationService.fetchIntegrationRules(
+ 'name',
+ AwsResource.eks
+ );
+ expect(api.get).toHaveBeenCalledWith(
+ cfg.getIntegrationRulesUrl('name', AwsResource.eks)
+ );
+ expect(response).toEqual({
+ nextKey: 'some-key',
+ rules: [
+ {
+ resourceType: 'eks',
+ region: 'us-west-2',
+ labelMatcher: [{ name: 'env', value: 'dev' }],
+ discoveryConfig: 'cfg',
+ lastSync: 1733782634,
+ },
+ ],
+ });
+
+ // test null response
+ jest.spyOn(api, 'get').mockResolvedValue(null);
+
+ response = await integrationService.fetchIntegrationRules(
+ 'name',
+ AwsResource.eks
+ );
+ expect(response).toEqual({
+ nextKey: undefined,
+ rules: [],
+ });
+});
+
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 48711112f39b8..f7fb6db4f4b2b 100644
--- a/web/packages/teleport/src/services/integrations/integrations.ts
+++ b/web/packages/teleport/src/services/integrations/integrations.ts
@@ -17,7 +17,9 @@
*/
import api from 'teleport/services/api';
-import cfg from 'teleport/config';
+import cfg, { UrlResourcesParams } from 'teleport/config';
+
+import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard';
import makeNode from '../nodes/makeNode';
import auth, { MfaChallengeScope } from '../auth/auth';
@@ -60,6 +62,7 @@ import {
AwsOidcPingResponse,
AwsOidcPingRequest,
IntegrationWithSummary,
+ IntegrationDiscoveryRules,
} from './types';
export const integrationService = {
@@ -428,6 +431,21 @@ export const integrationService = {
return resp;
});
},
+
+ fetchIntegrationRules(
+ name: string,
+ resourceType: AwsResource,
+ params?: UrlResourcesParams
+ ): Promise {
+ return api
+ .get(cfg.getIntegrationRulesUrl(name, resourceType, params))
+ .then(resp => {
+ return {
+ rules: resp?.rules || [],
+ 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 ff8f4347b985c..d63cac5c85a96 100644
--- a/web/packages/teleport/src/services/integrations/types.ts
+++ b/web/packages/teleport/src/services/integrations/types.ts
@@ -298,6 +298,31 @@ export type IntegrationWithSummary = {
awseks: ResourceTypeSummary;
};
+// IntegrationDiscoveryRules contains the list of discovery rules for a given Integration.
+export type IntegrationDiscoveryRules = {
+ // rules is the list of integration rules.
+ rules: IntegrationDiscoveryRule[];
+ // nextKey is the position to resume listing rules.
+ nextKey: string;
+};
+
+// IntegrationDiscoveryRule describes a discovery rule associated with an integration.
+export type IntegrationDiscoveryRule = {
+ // resourceType indicates the type of resource that this rule targets.
+ // This is the same value that is set in DiscoveryConfig.AWS..Types
+ // Example: ec2, rds, eks
+ resourceType: string;
+ // region where this rule applies to.
+ region: string;
+ // labelMatcher is the set of labels that are used to filter the resources before trying to auto-enroll them.
+ labelMatcher: Label[];
+ // discoveryConfig is the name of the DiscoveryConfig that created this rule.
+ discoveryConfig: string;
+ // lastSync contains the time when this rule was used.
+ // If empty, it indicates that the rule is not being used.
+ lastSync: number;
+};
+
// ResourceTypeSummary contains the summary of the enrollment rules and found resources by the integration.
export type ResourceTypeSummary = {
// rulesCount is the number of enrollment rules that are using this integration.