Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ad276a8
Add ACLs for workload identity
nicholasmarais1158 Sep 5, 2025
4ae65c0
Add list workload identities to webapi
nicholasmarais1158 Sep 5, 2025
ec33356
Add `newWebPackWithOptions`
nicholasmarais1158 Sep 8, 2025
9f81a86
Add sorting by name and spiffe id
nicholasmarais1158 Sep 8, 2025
07f4139
Add filter by search term
nicholasmarais1158 Sep 8, 2025
8e7628d
Use `require.ErrorContains`
nicholasmarais1158 Sep 9, 2025
d217a5f
Refactor `newWebPackWithOptions`
nicholasmarais1158 Sep 9, 2025
f1207b3
Merge branch 'master' into nicholasmarais1158/mwi-workload-id-list
nicholasmarais1158 Sep 9, 2025
c684522
Use `t.Context()`
nicholasmarais1158 Sep 9, 2025
b75965e
Add context to uses of `require.NoError` in loops
nicholasmarais1158 Sep 9, 2025
7b2f320
Tidy-up
nicholasmarais1158 Sep 9, 2025
77bc7fc
Un-deprecate `newWebPack`
nicholasmarais1158 Sep 10, 2025
732294c
Rename `KindWorkloadIdentity`
nicholasmarais1158 Sep 10, 2025
93553af
Add client-side API support for sort and filter
nicholasmarais1158 Sep 10, 2025
01c837a
Handle endpoint not supported scenario
nicholasmarais1158 Sep 10, 2025
d0479ac
Fix cache keys for spiffe id index
nicholasmarais1158 Sep 10, 2025
e5de35c
Add and use `ListWorkloadIdentitiesV2` RPC
nicholasmarais1158 Sep 10, 2025
90541bb
Return `CompareFailedError` for sorting unavailable
nicholasmarais1158 Sep 10, 2025
2c01c6d
Split sort into two fields (field and direction)
nicholasmarais1158 Sep 11, 2025
ac445a2
Update unsupported sort tests
nicholasmarais1158 Sep 11, 2025
a90cb3d
Make `updateQuery` callback optional on `SearchPanel`
nicholasmarais1158 Sep 10, 2025
5dd7740
Add workload identities list
nicholasmarais1158 Sep 10, 2025
aaa84a1
Add tests
nicholasmarais1158 Sep 10, 2025
4156c27
Add stories
nicholasmarais1158 Sep 10, 2025
05a0063
Rename nav item to "Workload Identities"
nicholasmarais1158 Sep 12, 2025
ffdb2c8
Revert change to conditional render (SearchPanel)
nicholasmarais1158 Sep 12, 2025
16f489a
Remove mono-spaced text
nicholasmarais1158 Sep 12, 2025
5e8c224
Suggested code refinements
nicholasmarais1158 Sep 12, 2025
842b9cb
Join spiffe_id page keys with a pipe
nicholasmarais1158 Sep 12, 2025
0a417d2
Join spiffe_id page keys with a pipe
nicholasmarais1158 Sep 12, 2025
4da26de
Revert "Join spiffe_id page keys with a pipe"
nicholasmarais1158 Sep 12, 2025
9495c8a
Base32 hex encode id for cache key
nicholasmarais1158 Sep 15, 2025
036084d
Merge branch 'nicholasmarais1158/mwi-workload-id-list' into nicholasm…
nicholasmarais1158 Sep 15, 2025
1265ced
Add missing useCallback dep
nicholasmarais1158 Sep 15, 2025
b3d80cc
Fix word break opportunities on Firefox
nicholasmarais1158 Sep 15, 2025
88a164d
Merge branch 'master' into nicholasmarais1158/mwi-workload-id-list-ui
nicholasmarais1158 Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions web/packages/shared/components/Search/SearchPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function SearchPanel({
hideAdvancedSearch,
extraChildren,
}: {
updateQuery(s: string): void;
updateSearch(s: string): void;
updateQuery?: (s: string) => void;
updateSearch: (s: string) => void;
pageIndicators?: { from: number; to: number; total: number };
filter: ResourceFilter;
disableSearch: boolean;
Expand All @@ -60,11 +60,11 @@ export function SearchPanel({
setQuery(newQuery);

if (isAdvancedSearch) {
updateQuery(newQuery);
updateQuery?.(newQuery);
return;
}

updateSearch(newQuery);
updateSearch?.(newQuery);
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export function BotInstancesList({
serversideSearchPanel: (
<SearchPanel
updateSearch={onSearchChange}
updateQuery={null}
hideAdvancedSearch={true}
filter={{ search: searchTerm }}
disableSearch={fetchStatus !== ''}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Teleport
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/

import { ReactElement } from 'react';

import { Cell, LabelCell } from 'design/DataTable/Cells';
import Table from 'design/DataTable/Table';
import { FetchingConfig, SortType } from 'design/DataTable/types';
import Flex from 'design/Flex';
import Text from 'design/Text';
import { SearchPanel } from 'shared/components/Search/SearchPanel';
import { CopyButton } from 'shared/components/UnifiedResources/shared/CopyButton';

import { WorkloadIdentity } from 'teleport/services/workloadIdentity/types';

export function WorkloadIdetitiesList({
data,
fetchStatus,
onFetchNext,
onFetchPrev,
sortType,
onSortChanged,
searchTerm,
onSearchChange,
}: {
data: WorkloadIdentity[];
sortType: SortType;
onSortChanged: (sortType: SortType) => void;
searchTerm: string;
onSearchChange: (term: string) => void;
} & Omit<FetchingConfig, 'onFetchMore'>) {
const tableData = data.map(d => ({
...d,
spiffe_hint: valueOrEmpty(d.spiffe_hint),
}));

return (
<Table<(typeof tableData)[number]>
data={tableData}
fetching={{
fetchStatus,
onFetchNext,
onFetchPrev,
disableLoadingIndicator: true,
}}
serversideProps={{
sort: sortType,
setSort: onSortChanged,
serversideSearchPanel: (
<SearchPanel
updateSearch={onSearchChange}
hideAdvancedSearch={true}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we hide advanced search?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not implemented for this endpoint. Only a text-contains search is supported.

filter={{ search: searchTerm }}
disableSearch={fetchStatus !== ''}
/>
),
}}
columns={[
{
key: 'name',
headerText: 'Name',
isSortable: true,
},
{
key: 'spiffe_id',
headerText: 'SPIFFE ID',
isSortable: true,
render: ({ spiffe_id }) => {
return spiffe_id ? (
<Cell>
<Flex inline alignItems={'center'} gap={1} mr={0}>
<Text>
{spiffe_id
.split('/')
.reduce<(ReactElement | string)[]>((acc, cur, i) => {
if (i === 0) {
acc.push(cur);
} else {
// Add break opportunities after each slash
acc.push('/', <wbr key={cur} />, cur);
}
return acc;
}, [])}
</Text>
<CopyButton name={spiffe_id} />
</Flex>
</Cell>
) : (
<Cell>{valueOrEmpty(spiffe_id)}</Cell>
);
},
},
{
key: 'labels',
headerText: 'Labels',
isSortable: false,
render: ({ labels: labelsMap }) => {
const labels = labelsMap ? Object.entries(labelsMap) : undefined;
return labels?.length ? (
<LabelCell data={labels.map(([k, v]) => `${k}: ${v || '-'}`)} />
) : (
<Cell>{valueOrEmpty('')}</Cell>
);
},
},
{
key: 'spiffe_hint',
headerText: 'Hint',
isSortable: false,
},
]}
emptyText="No workload identities found"
/>
);
}

function valueOrEmpty(value: string | null | undefined, empty = '-') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the empty parameter actually used anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but I felt the function was more complete this way and self-documenting.

return value || empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Teleport
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/

import { Meta, StoryObj } from '@storybook/react-vite';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createMemoryHistory } from 'history';
import { MemoryRouter, Route, Router } from 'react-router';

import cfg from 'teleport/config';
import { createTeleportContext } from 'teleport/mocks/contexts';
import { TeleportProviderBasic } from 'teleport/mocks/providers';
import { defaultAccess, makeAcl } from 'teleport/services/user/makeAcl';
import {
listWorkloadIdentitiesError,
listWorkloadIdentitiesForever,
listWorkloadIdentitiesSuccess,
} from 'teleport/test/helpers/workloadIdentities';

import { WorkloadIdentities } from './WorkloadIdentities';

const meta = {
title: 'Teleport/WorkloadIdentity',
component: Wrapper,
beforeEach: () => {
queryClient.clear(); // Prevent cached data sharing between stories
},
} satisfies Meta<typeof Wrapper>;

type Story = StoryObj<typeof meta>;

export default meta;

export const Happy: Story = {
parameters: {
msw: {
handlers: [listWorkloadIdentitiesSuccess()],
},
},
};

export const Empty: Story = {
parameters: {
msw: {
handlers: [
listWorkloadIdentitiesSuccess({
items: [],
next_page_token: null,
}),
],
},
},
};

export const NoListPermission: Story = {
args: { hasListPermission: false },
parameters: {
msw: {
handlers: [
/* should never make a call */
],
},
},
};

export const Error: Story = {
parameters: {
msw: {
handlers: [listWorkloadIdentitiesError(500, 'something went wrong')],
},
},
};

export const OutdatedProxy: Story = {
parameters: {
msw: {
handlers: [
listWorkloadIdentitiesError(404, 'path not found', {
proxyVersion: {
major: 18,
minor: 0,
patch: 0,
preRelease: '',
string: '18.0.0',
},
}),
],
},
},
};

export const UnsupportedSort: Story = {
parameters: {
msw: {
handlers: [
listWorkloadIdentitiesError(
400,
'unsupported sort, with some more info'
),
],
},
},
};

export const Loading: Story = {
parameters: {
msw: {
handlers: [listWorkloadIdentitiesForever()],
},
},
};

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
},
},
});

function Wrapper(props?: { hasListPermission?: boolean }) {
const { hasListPermission = true } = props ?? {};

const history = createMemoryHistory({
initialEntries: [cfg.routes.workloadIdentities],
});

const customAcl = makeAcl({
workloadIdentity: {
...defaultAccess,
list: hasListPermission,
},
});

const ctx = createTeleportContext({
customAcl,
});

return (
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<TeleportProviderBasic teleportCtx={ctx}>
<Router history={history}>
<Route path={cfg.routes.workloadIdentities}>
<WorkloadIdentities />
</Route>
</Router>
</TeleportProviderBasic>
</MemoryRouter>
</QueryClientProvider>
);
}
Loading
Loading