This commit is contained in:
Michael
2026-03-23 19:53:51 +00:00
committed by GitHub
parent 0f2dabf1ab
commit 1203d57ce8
17 changed files with 399 additions and 40 deletions
+27
View File
@@ -155,6 +155,17 @@ func TestIntegrationAthenaEventPagination(t *testing.T) {
})
}
func TestIntegrationAthenaSearchEventsBySearchTerm(t *testing.T) {
t.Run("sns", func(t *testing.T) {
const bypassSNSFalse = false
testIntegrationAthenaSearchEventsBySearchTerm(t, bypassSNSFalse)
})
t.Run("sqs", func(t *testing.T) {
const bypassSNSTrue = true
testIntegrationAthenaSearchEventsBySearchTerm(t, bypassSNSTrue)
})
}
func testIntegrationAthenaEventPagination(t *testing.T, bypassSNS bool) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
@@ -172,6 +183,22 @@ func testIntegrationAthenaEventPagination(t *testing.T, bypassSNS bool) {
eventsSuite.EventPagination(t)
}
func testIntegrationAthenaSearchEventsBySearchTerm(t *testing.T, bypassSNS bool) {
ctx := t.Context()
ac := SetupAthenaContext(t, ctx, AthenaContextConfig{BypassSNS: bypassSNS})
auditLogger := &EventuallyConsistentAuditLogger{
Inner: ac.log,
// Additional 5s is used to compensate for uploading parquet on s3.
QueryDelay: ac.batcherInterval + 5*time.Second,
}
eventsSuite := test.EventsSuite{
Log: auditLogger,
Clock: ac.clock,
}
eventsSuite.SearchEventsBySearchTerm(t)
}
func TestIntegrationAthenaLargeEvents(t *testing.T) {
t.Run("sns", func(t *testing.T) {
const bypassSNSFalse = false
+11 -3
View File
@@ -208,7 +208,7 @@ func (q *querier) SearchEvents(ctx context.Context, req events.SearchEventsReque
limit: req.Limit,
order: req.Order,
startKey: startKeyset,
filter: searchEventsFilter{eventTypes: req.EventTypes},
filter: searchEventsFilter{eventTypes: req.EventTypes, search: req.Search},
sessionID: "",
})
return events, keyset, trace.Wrap(err)
@@ -220,7 +220,7 @@ func (q *querier) SearchEvents(ctx context.Context, req events.SearchEventsReque
limit: req.Limit,
order: req.Order,
startKey: startKeyset,
filter: searchEventsFilter{eventTypes: req.EventTypes},
filter: searchEventsFilter{eventTypes: req.EventTypes, search: req.Search},
sessionID: "",
})
return events, keyset, trace.Wrap(err)
@@ -662,6 +662,7 @@ func (q *querier) searchEvents(ctx context.Context, req searchEventsRequest) ([]
type searchEventsFilter struct {
eventTypes []string
search string
condition utils.FieldsCondition
}
@@ -673,7 +674,8 @@ type queryBuilder struct {
// withTicks wraps string with ticks.
// string params in athena need to be wrapped by "ticks".
func withTicks(in string) string {
return fmt.Sprintf("'%s'", in)
escaped := strings.ReplaceAll(in, "'", "''")
return fmt.Sprintf("'%s'", escaped)
}
func sliceWithTicks(ss []string) []string {
@@ -740,6 +742,12 @@ func prepareQuery(params searchParams) (query string, execParams []string, err e
)
}
if params.filter.search != "" {
for term := range strings.FieldsSeq(strings.ToLower(params.filter.search)) {
qb.Append(" AND strpos(lower(event_data), ?) > 0", withTicks(term))
}
}
if params.order == types.EventOrderAscending {
if params.startKeyset != nil {
qb.Append(` AND (event_time, uid) > (?,?)`,
+51
View File
@@ -187,6 +187,57 @@ func TestSearchEvents(t *testing.T) {
wantQueryParams(t, mock, append(timeRangeParams, "'app.create'", "'app.delete'")...)
},
},
{
name: "query with search terms",
searchParams: &events.SearchEventsRequest{
From: fromUTC,
To: toUTC,
Limit: 100,
Search: "Root ALICE",
},
queryResultsResps: singleCallResults(100),
check: func(t *testing.T, mock *mockAthenaExecutor, paginationKey string) {
t.Helper()
wantSingleCallToAthena(t, mock)
wantQuery(t, mock, selectFromPrefix+whereTimeRange+
` AND strpos(lower(event_data), ?) > 0 AND strpos(lower(event_data), ?) > 0 ORDER BY event_time ASC, uid ASC LIMIT 100;`)
wantQueryParams(t, mock, append(timeRangeParams, "'root'", "'alice'")...)
},
},
{
name: "query with apostrophe in search term",
searchParams: &events.SearchEventsRequest{
From: fromUTC,
To: toUTC,
Limit: 100,
Search: "O'Connor",
},
queryResultsResps: singleCallResults(100),
check: func(t *testing.T, mock *mockAthenaExecutor, paginationKey string) {
t.Helper()
wantSingleCallToAthena(t, mock)
wantQuery(t, mock, selectFromPrefix+whereTimeRange+
` AND strpos(lower(event_data), ?) > 0 ORDER BY event_time ASC, uid ASC LIMIT 100;`)
wantQueryParams(t, mock, append(timeRangeParams, "'o''connor'")...)
},
},
{
name: "query with wildcard characters in search term",
searchParams: &events.SearchEventsRequest{
From: fromUTC,
To: toUTC,
Limit: 100,
Search: "alice_admin svc%prod",
},
queryResultsResps: singleCallResults(100),
check: func(t *testing.T, mock *mockAthenaExecutor, paginationKey string) {
t.Helper()
wantSingleCallToAthena(t, mock)
wantQuery(t, mock, selectFromPrefix+whereTimeRange+
` AND strpos(lower(event_data), ?) > 0 AND strpos(lower(event_data), ?) > 0 ORDER BY event_time ASC, uid ASC LIMIT 100;`)
wantQueryParams(t, mock, append(timeRangeParams, "'alice_admin'", "'svc%prod'")...)
},
},
{
name: "session id",
searchSessionParams: &events.SearchSessionEventsRequest{
+6 -2
View File
@@ -775,7 +775,7 @@ type legacyCheckpointKey struct {
//
// This function may never return more than 1 MiB of event data.
func (l *Log) SearchEvents(ctx context.Context, req events.SearchEventsRequest) ([]apievents.AuditEvent, string, error) {
values, next, err := l.searchEventsWithFilter(ctx, req.From, req.To, apidefaults.Namespace, req.Limit, req.Order, req.StartKey, searchEventsFilter{eventTypes: req.EventTypes}, "")
values, next, err := l.searchEventsWithFilter(ctx, req.From, req.To, apidefaults.Namespace, req.Limit, req.Order, req.StartKey, searchEventsFilter{eventTypes: req.EventTypes, search: req.Search}, "")
if err != nil {
return nil, "", trace.Wrap(err)
}
@@ -787,7 +787,7 @@ func (l *Log) SearchEvents(ctx context.Context, req events.SearchEventsRequest)
}
func (l *Log) SearchUnstructuredEvents(ctx context.Context, req events.SearchEventsRequest) ([]*auditlogpb.EventUnstructured, string, error) {
values, next, err := l.searchEventsWithFilter(ctx, req.From, req.To, apidefaults.Namespace, req.Limit, req.Order, req.StartKey, searchEventsFilter{eventTypes: req.EventTypes}, "")
values, next, err := l.searchEventsWithFilter(ctx, req.From, req.To, apidefaults.Namespace, req.Limit, req.Order, req.StartKey, searchEventsFilter{eventTypes: req.EventTypes, search: req.Search}, "")
if err != nil {
return nil, "", trace.Wrap(err)
}
@@ -1109,6 +1109,7 @@ func (l *Log) SearchSessionEvents(ctx context.Context, req events.SearchSessionE
type searchEventsFilter struct {
eventTypes []string
search string
condExpr string
condParams condFilterParams
filterFunc utils.FieldsCondition
@@ -1618,6 +1619,9 @@ func (l *eventsFetcher) processQueryOutput(output *dynamodb.QueryOutput) ([]even
if err != nil {
return nil, false, trace.Wrap(err)
}
if l.filter.search != "" && !events.MatchSearch(l.filter.search, string(data)) {
continue
}
// Stop early when the fetcher's total size exceeds the response size limit.
if l.totalSize+len(data) > events.MaxEventBytesInResponse {
@@ -130,6 +130,12 @@ func TestSearchSessionEvensBySessionID(t *testing.T) {
tt.suite.SearchSessionEventsBySessionID(t)
}
func TestSearchEventsBySearchTerm(t *testing.T) {
tt := setupDynamoContext(t)
tt.suite.SearchEventsBySearchTerm(t)
}
// TestCheckpointOutsideOfWindow tests if [Log] doesn't panic
// if checkpoint date is outside of the window [fromUTC,toUTC].
func TestCheckpointOutsideOfWindow(t *testing.T) {
+19 -12
View File
@@ -716,18 +716,7 @@ func (l *FileLog) findInFile(path string, filter searchEventsFilter) ([]EventFie
}
// Check if search filter matches.
if accepted && filter.search != "" {
eventJSON := strings.ToLower(string(scanner.Bytes()))
searchTerms := strings.Fields(strings.ToLower(filter.search))
matchedAll := true
for _, term := range searchTerms {
if !strings.Contains(eventJSON, term) {
matchedAll = false
break
}
}
accepted = matchedAll
accepted = MatchSearch(filter.search, string(scanner.Bytes()))
}
if accepted {
@@ -782,6 +771,24 @@ func (f ByTimeAndIndex) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}
// MatchSearch returns true if all whitespace-delimited search terms are present
// in text, using case-insensitive substring matching.
func MatchSearch(search, text string) bool {
terms := strings.Fields(strings.ToLower(search))
if len(terms) == 0 {
return true
}
text = strings.ToLower(text)
for _, term := range terms {
if !strings.Contains(text, term) {
return false
}
}
return true
}
// getTime converts json time to string
func getTime(v any) time.Time {
sval, ok := v.(string)
+52
View File
@@ -274,6 +274,58 @@ func TestFileLogCheckpoint(t *testing.T) {
}
}
func TestMatchSearch(t *testing.T) {
tests := []struct {
name string
search string
text string
want bool
}{
{
name: "empty search matches any text",
search: "",
text: `{"user":"alice"}`,
want: true,
},
{
name: "whitespace-only search matches any text",
search: " \t\n ",
text: `{"user":"alice"}`,
want: true,
},
{
name: "single term is case-insensitive",
search: "ALICE",
text: `{"user":"alice-search-target"}`,
want: true,
},
{
name: "all search terms must be present",
search: "target alice",
text: `{"user":"alice-search-target"}`,
want: true,
},
{
name: "missing term does not match",
search: "target carol",
text: `{"user":"alice-search-target"}`,
want: false,
},
{
name: "wildcard-like characters are treated literally",
search: "svc%prod",
text: `route svc%prod backend`,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, MatchSearch(tt.search, tt.text))
})
}
}
func TestSearchSessionEvents(t *testing.T) {
clock := clockwork.NewFakeClock()
start := clock.Now()
+31 -3
View File
@@ -369,7 +369,7 @@ func (l *Log) SearchEvents(ctx context.Context, req events.SearchEventsRequest)
limit: req.Limit,
order: req.Order,
lastKey: req.StartKey,
filter: searchEventsFilter{eventTypes: req.EventTypes},
filter: searchEventsFilter{eventTypes: req.EventTypes, search: req.Search},
sessionID: "",
})
if err != nil {
@@ -479,6 +479,20 @@ func (l *Log) query(
docID = checkpointParts[1]
}
getCheckpointTime := func(docSnap *firestore.DocumentSnapshot) (int64, error) {
createdAtValue, ok := docSnap.Data()[createdAtDocProperty]
if !ok {
return 0, trace.BadParameter("missing %q field in event document %q", createdAtDocProperty, docSnap.Ref.ID)
}
checkpointTime, ok := createdAtValue.(int64)
if !ok {
return 0, trace.BadParameter("invalid %q type %T in event document %q", createdAtDocProperty, createdAtValue, docSnap.Ref.ID)
}
return checkpointTime, nil
}
for {
if lastKey != "" {
query = query.StartAfter(checkpointTime, docID)
@@ -520,13 +534,26 @@ func (l *Log) query(
return nil, "", trace.Errorf("failed to unmarshal event %v", err)
}
if filter.search != "" && !events.MatchSearch(filter.search, string(data)) {
checkpointTime, err = getCheckpointTime(docSnap)
if err != nil {
return nil, "", trace.Wrap(err)
}
docID = docSnap.Ref.ID
lastKey = strconv.FormatInt(checkpointTime, 10) + ":" + docID
continue
}
// if the total size of the events exceeds the limit, return the events
// collected so far and the last key to resume the query.
if totalSize+len(data) >= events.MaxEventBytesInResponse {
return values, lastKey, nil
}
checkpointTime = docSnap.Data()[createdAtDocProperty].(int64)
checkpointTime, err = getCheckpointTime(docSnap)
if err != nil {
return nil, "", trace.Wrap(err)
}
docID = docSnap.Ref.ID
lastKey = strconv.FormatInt(checkpointTime, 10) + ":" + docID
@@ -588,6 +615,7 @@ func (l *Log) GetEventExportChunks(ctx context.Context, req *auditlogpb.GetEvent
type searchEventsFilter struct {
eventTypes []string
search string
condition utils.FieldsCondition
}
@@ -663,7 +691,7 @@ func (l *Log) SearchUnstructuredEvents(ctx context.Context, req events.SearchEve
limit: req.Limit,
order: req.Order,
lastKey: req.StartKey,
filter: searchEventsFilter{eventTypes: req.EventTypes},
filter: searchEventsFilter{eventTypes: req.EventTypes, search: req.Search},
sessionID: "",
})
@@ -129,12 +129,18 @@ func (tt *firestoreContext) testSearchSessionEvensBySessionID(t *testing.T) {
tt.suite.SearchSessionEventsBySessionID(t)
}
func (tt *firestoreContext) testSearchEventsBySearchTerm(t *testing.T) {
tt.setupTest(t)
tt.suite.SearchEventsBySearchTerm(t)
}
func TestFirestoreEvents(t *testing.T) {
tt := setupFirestoreContext(t)
t.Run("TestSessionEventsCRUD", tt.testSessionEventsCRUD)
t.Run("TestPagination", tt.testPagination)
t.Run("TestSearchSessionEvensBySessionID", tt.testSearchSessionEvensBySessionID)
t.Run("TestSearchEventsBySearchTerm", tt.testSearchEventsBySearchTerm)
}
func emulatorRunning() bool {
+13 -4
View File
@@ -428,7 +428,7 @@ func (l *Log) EmitAuditEvent(ctx context.Context, event apievents.AuditEvent) er
func (l *Log) searchEvents(
ctx context.Context,
fromTime, toTime time.Time,
eventTypes []string, cond *utils.ToFieldsConditionConfig, sessionID string,
eventTypes []string, cond *utils.ToFieldsConditionConfig, sessionID, search string,
limit int, order types.EventOrder, startKey string,
) ([]events.EventFields, string, error) {
if limit <= 0 {
@@ -457,6 +457,8 @@ func (l *Log) searchEvents(
}
}
searchTerms := strings.Fields(strings.ToLower(search))
sessionUUID := l.deriveSessionID(ctx, sessionID)
var qb strings.Builder
@@ -473,6 +475,9 @@ func (l *Log) searchEvents(
// no matter what the argument is
qb.WriteString(" AND events.session_id != '00000000-0000-0000-0000-000000000000' AND events.session_id = @session_id")
}
for i := range searchTerms {
fmt.Fprintf(&qb, " AND POSITION(@search_term_%d IN lower(events.event_data::text)) > 0", i)
}
if order != types.EventOrderDescending {
if startKey != "" {
qb.WriteString(" AND (events.event_time, events.event_id) > (@start_time, @start_id)")
@@ -494,6 +499,9 @@ func (l *Log) searchEvents(
"start_time": startTime,
"start_id": startID,
}
for i, term := range searchTerms {
queryArgs[fmt.Sprintf("search_term_%d", i)] = term
}
const fetchSize = defaults.EventsIterationLimit
fetchQuery := fmt.Sprintf("FETCH %d FROM cur", fetchSize)
@@ -588,7 +596,7 @@ func (l *Log) SearchEvents(ctx context.Context, req events.SearchEventsRequest)
var emptyCond *utils.ToFieldsConditionConfig
const emptySessionID = ""
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, req.EventTypes, emptyCond, emptySessionID, req.Limit, req.Order, req.StartKey)
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, req.EventTypes, emptyCond, emptySessionID, req.Search, req.Limit, req.Order, req.StartKey)
if err != nil {
return nil, next, trace.Wrap(err)
}
@@ -605,7 +613,7 @@ func (l *Log) SearchUnstructuredEvents(ctx context.Context, req events.SearchEve
var emptyCond *utils.ToFieldsConditionConfig
const emptySessionID = ""
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, req.EventTypes, emptyCond, emptySessionID, req.Limit, req.Order, req.StartKey)
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, req.EventTypes, emptyCond, emptySessionID, req.Search, req.Limit, req.Order, req.StartKey)
if err != nil {
return nil, next, trace.Wrap(err)
}
@@ -626,7 +634,8 @@ func (l *Log) GetEventExportChunks(ctx context.Context, req *auditlogpb.GetEvent
// SearchSessionEvents implements [events.AuditLogger].
func (l *Log) SearchSessionEvents(ctx context.Context, req events.SearchSessionEventsRequest) ([]apievents.AuditEvent, string, error) {
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, events.SessionRecordingEvents, req.Cond, req.SessionID, req.Limit, req.Order, req.StartKey)
const emptySearch = ""
evtsRaw, next, err := l.searchEvents(ctx, req.From, req.To, events.SessionRecordingEvents, req.Cond, req.SessionID, emptySearch, req.Limit, req.Order, req.StartKey)
if err != nil {
return nil, next, trace.Wrap(err)
}
+6 -1
View File
@@ -78,6 +78,10 @@ func TestPostgresEvents(t *testing.T) {
truncateEvents(t)
suite.SearchSessionEventsBySessionID(t)
})
t.Run("SearchEventsBySearchTerm", func(t *testing.T) {
truncateEvents(t)
suite.SearchEventsBySearchTerm(t)
})
}
// TestLog_nonStandardSessionID tests for
@@ -136,7 +140,8 @@ func TestLog_nonStandardSessionID(t *testing.T) {
[]string{appStartEvent.Metadata.Type}, // eventTypes
nil, // cond
appStartEvent.SessionID,
2, // limit
"", // search
2, // limit
types.EventOrderAscending,
"", // startKey
)
+51
View File
@@ -519,6 +519,57 @@ Outer:
}
}
func (s *EventsSuite) SearchEventsBySearchTerm(t *testing.T) {
ctx := t.Context()
baseTime := time.Now().UTC()
testUsers := []string{"alice-search-target", "bob-search-target"}
for i, user := range testUsers {
err := s.Log.EmitAuditEvent(ctx, &apievents.UserLogin{
Method: events.LoginMethodSAML,
Status: apievents.Status{Success: true},
UserMetadata: apievents.UserMetadata{User: user},
Metadata: apievents.Metadata{
ID: uuid.NewString(),
Type: events.UserLoginEvent,
Time: baseTime.Add(time.Second * time.Duration(i)),
},
})
require.NoError(t, err)
}
if s.QueryDelay != 0 {
time.Sleep(s.QueryDelay)
}
searchAndAssertUsers := func(search string, wantUsers ...string) {
require.EventuallyWithT(t, func(t *assert.CollectT) {
history, _, err := s.Log.SearchEvents(ctx, events.SearchEventsRequest{
From: baseTime.Add(-1 * time.Minute),
To: baseTime.Add(time.Hour),
Limit: 100,
Order: types.EventOrderAscending,
Search: search,
})
require.NoError(t, err)
gotUsers := make([]string, 0, len(history))
for _, event := range history {
loginEvent, ok := event.(*apievents.UserLogin)
require.True(t, ok)
gotUsers = append(gotUsers, loginEvent.User)
}
require.ElementsMatch(t, wantUsers, gotUsers)
}, 30*time.Second, 500*time.Millisecond)
}
searchAndAssertUsers("alice-search-target", "alice-search-target")
searchAndAssertUsers("search-target alice", "alice-search-target")
searchAndAssertUsers("search-target", "alice-search-target", "bob-search-target")
searchAndAssertUsers("search-target carol")
}
// SessionEventsCRUD covers session events
func (s *EventsSuite) SessionEventsCRUD(t *testing.T) {
loginTime := s.Clock.Now().UTC()
+77 -1
View File
@@ -31,7 +31,8 @@ import TeleportContext from 'teleport/teleportContext';
import { renderWithMemoryRouter } from 'teleport/test/helpers/router';
import { ContextProvider } from '..';
import { AuditContainer } from './Audit';
import { Audit, AuditContainer } from './Audit';
import type { State } from './useAuditEvents';
const mio = mockIntersectionObserver();
@@ -93,6 +94,54 @@ describe('Audit', () => {
expect(router.state.location.pathname).toBe('/web/cluster/root/audit');
expect(router.state.location.search).toContain('order=ASC');
});
it('does not fetch next page while placeholder data is shown', async () => {
const ctx = createTeleportContext();
jest
.spyOn(ctx.clusterService, 'fetchClusters')
.mockImplementation(() => new Promise(() => {}));
const fetchNextPage = jest.fn();
renderWithMemoryRouter(
<Audit
{...makeState(ctx, {
events: [
makeEvent({
codeDesc: 'Local Login',
message: 'Local user [root] successfully logged in',
id: 'user.login:2021-05-25T14:37:27.848Z',
code: 'T1000I',
user: 'root',
time: new Date('2021-05-25T14:37:27.848Z'),
raw: {
cluster_name: 'im-a-cluster-name',
code: 'T1000I',
ei: 0,
event: 'user.login',
method: 'local',
success: true,
time: '2021-05-25T14:37:27.848Z',
user: 'root',
},
}),
],
hasNextPage: true,
isPlaceholderData: true,
fetchNextPage,
})}
/>,
{
path: cfg.routes.audit,
initialEntries: ['/web/cluster/root/audit'],
wrapper: makeWrapper({ ctx }),
}
);
act(mio.enterAll);
expect(fetchNextPage).not.toHaveBeenCalled();
});
});
function renderComponent(ctx: TeleportContext) {
@@ -116,3 +165,30 @@ function makeWrapper({ ctx }: { ctx: TeleportContext }) {
);
};
}
function makeState(
ctx: TeleportContext,
overrides: Partial<State> = {}
): State {
return {
events: [],
fetchNextPage: jest.fn(),
hasNextPage: false,
isPlaceholderData: false,
isFetchingNextPage: false,
isLoading: false,
error: null,
isSuccess: true,
refetch: jest.fn(),
isError: false,
clusterId: 'root',
range: undefined,
setRange: jest.fn(),
search: '',
setSearch: jest.fn(),
sort: { fieldName: 'time', dir: 'DESC' },
setSort: jest.fn(),
ctx,
...overrides,
};
}
+14 -7
View File
@@ -54,6 +54,7 @@ export function Audit(props: State) {
clusterId,
fetchNextPage,
hasNextPage,
isPlaceholderData,
isFetchingNextPage,
error,
isLoading,
@@ -68,9 +69,12 @@ export function Audit(props: State) {
const [errorMessage, setErrorMessage] = useState('');
const canFetchNextPage =
hasNextPage && !isFetchingNextPage && !isError && !isPlaceholderData;
const { setTrigger } = useInfiniteScroll({
fetch: async () => {
if (hasNextPage && !isFetchingNextPage && !isError) {
if (canFetchNextPage) {
fetchNextPage();
}
},
@@ -81,11 +85,16 @@ export function Audit(props: State) {
};
const onLoadMoreClicked = () => {
if (hasNextPage && !isFetchingNextPage) {
if (canFetchNextPage) {
fetchNextPage();
}
};
const showSkeleton =
(isLoading && events.length === 0) ||
isFetchingNextPage ||
isPlaceholderData;
return (
<FeatureBox unsetHeight>
<FeatureHeader alignItems="center">
@@ -121,7 +130,7 @@ export function Audit(props: State) {
hideAdvancedSearch={true}
filter={{ search }}
/>
{!isLoading && (
{!isLoading && !isPlaceholderData && (
<EventList
events={events}
search={search}
@@ -130,10 +139,8 @@ export function Audit(props: State) {
setSort={setSort}
/>
)}
{((isLoading && events.length === 0) || isFetchingNextPage) && (
<EventListSkeleton />
)}
<div ref={setTrigger} />
{showSkeleton && <EventListSkeleton />}
{!isPlaceholderData && <div ref={setTrigger} />}
{isError && events.length > 0 && !isLoading && (
<Box mt={2} textAlign="center">
<ButtonSecondary onClick={onLoadMoreClicked}>
@@ -18,7 +18,7 @@
import { useState } from 'react';
import { ButtonBorder, Flex } from 'design';
import { ButtonBorder, Flex, Text } from 'design';
import Table, { Cell } from 'design/DataTable';
import { Event } from 'teleport/services/audit';
@@ -38,7 +38,7 @@ export default function EventList(props: Props) {
columns={[
{
key: 'codeDesc',
headerText: 'Type',
headerText: 'Event',
isSortable: false,
render: event => renderTypeCell(event),
},
@@ -83,7 +83,7 @@ export const renderActionCell = (
<Flex gap={2} justifyContent="flex-end">
<ViewInPolicyButton event={event} />
<ButtonBorder
size="small"
size="medium"
onClick={() => onShowDetails(event)}
width="87px"
>
@@ -94,11 +94,17 @@ export const renderActionCell = (
);
export const renderTimeCell = ({ time }: Event) => (
<Cell style={{ minWidth: '120px' }}>{time.toISOString()}</Cell>
<Cell style={{ minWidth: '120px' }}>
<Text color="text.slightlyMuted">{time.toISOString()}</Text>
</Cell>
);
export function renderDescCell({ message }: Event) {
return <Cell style={{ wordBreak: 'break-word' }}>{message}</Cell>;
return (
<Cell style={{ wordBreak: 'break-word' }}>
<Text color="text.slightlyMuted">{message}</Text>
</Cell>
);
}
type Props = {
@@ -19,7 +19,9 @@
import styled from 'styled-components';
import { Cell } from 'design/DataTable';
import Flex from 'design/Flex';
import * as Icons from 'design/Icon';
import Text from 'design/Text/Text';
import { Event, EventCode, eventCodes } from 'teleport/services/audit';
@@ -372,7 +374,7 @@ export default function renderTypeCell(event: Event) {
const Icon = EventIconMap[event.code] || Icons.ListThin;
const iconProps = {
p: 1,
p: 2,
mr: 3,
};
@@ -380,7 +382,19 @@ export default function renderTypeCell(event: Event) {
<Cell style={{ verticalAlign: 'inherit' }}>
<StyledEventType>
<Icon {...iconProps} size="medium" />
{event.codeDesc}
<Flex
gap={0}
flexDirection="column"
minHeight={64}
justifyContent="center"
>
<Text typography="body2" fontWeight={500} mb={0}>
{event.raw.event}
</Text>
<Text typography="body2" color="text.muted">
{event.codeDesc}
</Text>
</Flex>
</StyledEventType>
</Cell>
);
@@ -55,6 +55,7 @@ export default function useAuditEvents(
error,
fetchNextPage,
hasNextPage,
isPlaceholderData,
isFetchingNextPage,
isLoading,
isSuccess,
@@ -152,6 +153,7 @@ export default function useAuditEvents(
events,
fetchNextPage,
hasNextPage,
isPlaceholderData,
isFetchingNextPage,
isLoading,
error,