Skip to content

Commit

Permalink
Add search bar and timestamp sort to Audit Logs UI
Browse files Browse the repository at this point in the history
This commit adds a search bar to the Audit Logs UI and restores the
ability to sort all logs by timestamp. In addition to these changes,
the style of the dropdowns themselves have been slightly polished,
including the "Filter by" text being centered and the empty logs text
being more clear.

Co-authored-by: Wendy Bujalski <[email protected]>
Signed-off-by: Nick Gerace <[email protected]>
  • Loading branch information
nickgerace and wendybujalski committed Dec 16, 2024
1 parent d6b386d commit 5797631
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 65 deletions.
46 changes: 33 additions & 13 deletions app/web/src/components/AuditLogHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,18 @@
<DropdownMenu
v-if="header.id !== 'timestamp' && header.id !== 'json'"
ref="dropdownMenuRef"
:items="dropdownMenuItems"
:items="filteredDropdownMenuItems"
:anchorTo="{ $el: thRef }"
:search="dropdownMenuItems.length > 0"
alignCenter
/>
@search="onSearch"
>
<DropdownMenuItem
v-if="filteredDropdownMenuItems.length < 1"
header
label="No matching filters found"
/>
</DropdownMenu>
</template>
</div>
</th>
Expand All @@ -72,6 +80,7 @@
<script lang="ts" setup>
import {
DropdownMenu,
DropdownMenuItem,
DropdownMenuItemObjectDef,
IconButton,
themeClasses,
Expand Down Expand Up @@ -110,12 +119,10 @@ const logsStore = useLogsStore();
const label = computed(() => props.header.column.columnDef.header as string);
const icon = computed(() => {
// NOTE(nick): restore timestamp sort after audit trail is shipped.
// if (props.header.id === "timestamp") {
// if (props.filters.sortTimestampAscending) return "chevron--up";
// else return "chevron--down";
// } else if (selectedFilters.value.length > 0) {
if (selectedFilters.value.length > 0) {
if (props.header.id === "timestamp") {
if (logsStore.sortAscending) return "chevron--up";
return "chevron--down";
} else if (selectedFilters.value.length > 0) {
return "filter";
}
return "none";
Expand Down Expand Up @@ -150,11 +157,9 @@ const selectedFilters = computed(() => {
const headerText = computed(() => {
if (label.value === "Time") {
// NOTE(nick): restore timestamp sort after audit trail is shipped.
// return `Sorting By Timestamp ${
// props.filters.sortTimestampAscending ? "(Oldest)" : "(Newest)"
// }`;
return "Sorted by Timestamp (Newest)";
return `Sorting By Timestamp ${
logsStore.sortAscending ? "(Oldest)" : "(Newest)"
}`;
}
if (selectedFilters.value.length > 0) {
return `Filtering by ${selectedFilters.value.length} selection${
Expand Down Expand Up @@ -189,6 +194,8 @@ const dropdownMenuItems = computed(() => {
items.push({
label: headerText.value,
header: true,
centerHeader: true,
disableCheckable: true,
});
for (const k of filterOptions.value) {
Expand All @@ -214,6 +221,19 @@ const dropdownMenuItems = computed(() => {
return items;
});
const searchString = ref("");
const onSearch = (search: string) => {
searchString.value = search.trim().toLocaleLowerCase();
};
const filteredDropdownMenuItems = computed(() => {
if (!dropdownMenuRef.value || searchString.value === "")
return dropdownMenuItems.value;
return dropdownMenuItems.value.filter((item) =>
item.label?.toLocaleLowerCase().includes(searchString.value),
);
});
const onClick = () => {
dropdownMenuRef.value?.open();
emit("select");
Expand Down
75 changes: 38 additions & 37 deletions app/web/src/components/Workspace/WorkspaceAuditLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
</template>
</tbody>
</table>
<template v-if="initialLoadRequestStatus.isSuccess">
<template v-if="initialLoadLogsRequestStatus.isSuccess">
<span
v-if="noRowsMessage"
class="flex flex-row items-center justify-center pt-md"
Expand All @@ -155,14 +155,14 @@
:disabled="!canLoadMore"
:label="canLoadMore ? 'Load 50 More' : 'All Entries Loaded'"
loadingText="Loading More Logs..."
:requestStatus="loadMoreRequestStatus"
@click="loadMore()"
:requestStatus="loadLogsRequestStatus"
@click="loadLogs(true, false)"
/>
</div>
</template>
<RequestStatusMessage
v-else
:requestStatus="initialLoadRequestStatus"
:requestStatus="initialLoadLogsRequestStatus"
loadingMessage="Loading Logs..."
/>
</ScrollArea>
Expand Down Expand Up @@ -197,7 +197,7 @@ const changeSetsStore = useChangeSetsStore();
const logsStore = useLogsStore();
const logs = computed(() => logsStore.logs);
const size = computed(() => logsStore.size);
const sizeForWatcher = computed(() => logsStore.size);
const canLoadMore = computed(() => logsStore.canLoadMore);
const selectedChangeSetName = computed(
Expand All @@ -213,38 +213,41 @@ const collapseAllRows = () => {
rowCollapseState.value = new Array(logs.value.length).fill(false);
};
// TODO(nick): restore pagination once the audit trail feature is shipped.
// const loadLogs = async () => {
// collapseAllRows();
// logsStore.LOAD_PAGE(size.value);
// trackEvent("load-audit-logs", { size: size.value });
// };
const initialLoadRequestIdentifier = "initialLoad";
const initialLoadRequestStatus = logsStore.getRequestStatus(
const initialLoadLogsRequestIdentifier = "initialLoadLogs";
const initialLoadLogsRequestStatus = logsStore.getRequestStatus(
"LOAD_PAGE",
initialLoadRequestIdentifier,
initialLoadLogsRequestIdentifier,
);
const performInitialLoad = async () => {
const performInitialLoadLogs = async () => {
collapseAllRows();
logsStore.LOAD_PAGE(size.value, initialLoadRequestIdentifier);
trackEvent("load-audit-logs", { size: size.value });
const size = logsStore.size;
const sortAscending = logsStore.sortAscending;
logsStore.LOAD_PAGE(size, sortAscending, initialLoadLogsRequestIdentifier);
trackEvent("load-audit-logs", { size, sortAscending });
};
const loadMoreRequestIdentifier = "loadMore";
const loadMoreRequestStatus = logsStore.getRequestStatus(
const loadLogsRequestIdentifier = "loadLogs";
const loadLogsRequestStatus = logsStore.getRequestStatus(
"LOAD_PAGE",
loadMoreRequestIdentifier,
loadLogsRequestIdentifier,
);
const loadMore = async () => {
logsStore.size += 50;
const newSize = logsStore.size;
logsStore.LOAD_PAGE(newSize, loadMoreRequestIdentifier);
trackEvent("load-audit-logs", { size: newSize });
const loadLogs = async (expandSize: boolean, toggleTimestampSort: boolean) => {
if (expandSize === true) {
logsStore.size += 50;
}
if (toggleTimestampSort) {
logsStore.sortAscending = !logsStore.sortAscending;
}
const size = logsStore.size;
const sortAscending = logsStore.sortAscending;
logsStore.LOAD_PAGE(size, sortAscending, loadLogsRequestIdentifier);
trackEvent("load-audit-logs", { size, sortAscending });
};
// Load the logs when this component is loaded.
performInitialLoad();
performInitialLoadLogs();
const columnHelper = createColumnHelper<AuditLogDisplay>();
Expand Down Expand Up @@ -319,18 +322,15 @@ const table = useVueTable({
getFilteredRowModel: getFilteredRowModel(),
});
table.setPageSize(size.value);
watch(size, (size) => {
table.setPageSize(size);
table.setPageSize(sizeForWatcher.value);
watch(sizeForWatcher, (sizeForWatcher) => {
table.setPageSize(sizeForWatcher);
});
const onHeaderClick = (id: string) => {
// NOTE(nick): restore timestamp sort after the audit trail feature is shipped.
// if (id === "timestamp") {
// logsStore.filters.sortTimestampAscending = !logsStore.filters.sortTimestampAscending;
// loadLogs();
// } else if (id === "json" && anyRowsOpen.value) {
if (id === "json" && anyRowsOpen.value) {
if (id === "timestamp") {
loadLogs(false, true);
} else if (id === "json" && anyRowsOpen.value) {
collapseAllRows();
}
};
Expand Down Expand Up @@ -387,7 +387,8 @@ const clearFilters = (id: string) => {
};
const noRowsMessage = computed(() => {
if (logs.value.length < 1) return "No logs exist in the workspace.";
if (logs.value.length < 1)
return "No logs exist for the selected Change Set.";
if (table.getRowModel().rows.length === 0)
return "No entries match selected filter criteria.";
return null;
Expand Down
31 changes: 24 additions & 7 deletions app/web/src/store/logs.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ export const useLogsStore = (forceChangeSetId?: ChangeSetId) => {
`ws${workspaceId || "NONE"}/cs${changeSetId || "NONE"}/audit-logs`,
{
state: () => ({
logs: [] as AuditLogDisplay[],
size: 50 as number,
sortAscending: false as boolean,
logs: [] as AuditLogDisplay[],
canLoadMore: true as boolean,
filters: {
changeSetFilter: [],
Expand All @@ -105,10 +106,14 @@ export const useLogsStore = (forceChangeSetId?: ChangeSetId) => {
} as AuditLogHeaderOptions,
}),
actions: {
async LOAD_PAGE(size: number, identifier?: string) {
async LOAD_PAGE(
size: number,
sortAscending: boolean,
identifier?: string,
) {
return new ApiRequest<{ logs: AuditLog[]; canLoadMore: boolean }>({
url: API_PREFIX,
params: { ...visibility, size },
params: { ...visibility, size, sortAscending },
keyRequestStatusBy: identifier,
method: "get",
onSuccess: (response) => {
Expand Down Expand Up @@ -191,10 +196,14 @@ export const useLogsStore = (forceChangeSetId?: ChangeSetId) => {
},
});
},
enqueueLoadPage(size: number, identifier: string) {
enqueueLoadPage(
size: number,
sortAscending: boolean,
identifier: string,
) {
if (!debouncer) {
debouncer = keyedDebouncer((identifier: string) => {
this.LOAD_PAGE(size, identifier);
this.LOAD_PAGE(size, sortAscending, identifier);
}, 500);
}
const loadPage = debouncer(identifier);
Expand All @@ -214,13 +223,21 @@ export const useLogsStore = (forceChangeSetId?: ChangeSetId) => {
// If the change set of the event is the same as ours, then let's reload. Otherwise, let's only
// reload if we are on HEAD and the change set has been applied or abandoned.
if (changeSetId === payload.changeSetId) {
this.enqueueLoadPage(this.size, "event");
this.enqueueLoadPage(
this.size,
this.sortAscending,
"event",
);
} else if (
changeSetId === changeSetsStore.headChangeSetId &&
(payload.changeSetStatus === "Applied" ||
payload.changeSetStatus === "Abandoned")
) {
this.enqueueLoadPage(this.size, "event");
this.enqueueLoadPage(
this.size,
this.sortAscending,
"event",
);
}
}
},
Expand Down
12 changes: 8 additions & 4 deletions lib/audit-database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ impl AuditLogRow {
workspace_id: WorkspacePk,
change_set_ids: Vec<ChangeSetId>,
size: usize,
sort_ascending: bool,
) -> Result<(Vec<Self>, bool)> {
let size = size as i64;
let change_set_ids: Vec<String> = change_set_ids.iter().map(|id| id.to_string()).collect();
Expand All @@ -199,12 +200,15 @@ impl AuditLogRow {
let count: i64 = row.try_get("count")?;
let can_load_more = count > size;

let query = if sort_ascending {
"SELECT * from audit_logs WHERE workspace_id = $1 AND change_set_id = ANY($2) ORDER BY timestamp ASC LIMIT $3"
} else {
"SELECT * from audit_logs WHERE workspace_id = $1 AND change_set_id = ANY($2) ORDER BY timestamp DESC LIMIT $3"
};

let mut result = Vec::new();
let rows = client
.query(
"SELECT * from audit_logs WHERE workspace_id = $1 AND change_set_id = ANY($2) ORDER BY timestamp DESC LIMIT $3",
&[&workspace_id, &change_set_ids, &size],
)
.query(query, &[&workspace_id, &change_set_ids, &size])
.await?;
for row in rows {
result.push(Self::try_from(row)?);
Expand Down
2 changes: 1 addition & 1 deletion lib/dal-test/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ pub async fn list_audit_logs_until_expected_number_of_rows(
let mut actual_number_of_rows = 0;

while start.elapsed() < timeout {
let (audit_logs, _) = audit_logging::list(ctx, context, size).await?;
let (audit_logs, _) = audit_logging::list(ctx, context, size, false).await?;
actual_number_of_rows = audit_logs.len();
if actual_number_of_rows == expected_number_of_rows {
return Ok(audit_logs);
Expand Down
10 changes: 9 additions & 1 deletion lib/dal/src/audit_logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ pub async fn list(
ctx: &DalContext,
audit_database_context: &AuditDatabaseContext,
size: usize,
sort_ascending: bool,
) -> Result<(Vec<AuditLogRow>, bool)> {
let workspace_id = ctx.workspace_pk().map_err(Box::new)?;
let change_set_id = ctx.change_set_id();
Expand All @@ -275,7 +276,14 @@ pub async fn list(
change_set_ids
};

Ok(AuditLogRow::list(audit_database_context, workspace_id, change_set_ids, size).await?)
Ok(AuditLogRow::list(
audit_database_context,
workspace_id,
change_set_ids,
size,
sort_ascending,
)
.await?)
}

#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
Expand Down
2 changes: 2 additions & 0 deletions lib/sdf-server/src/service/v2/audit_log/list_audit_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
#[serde(rename_all = "camelCase")]
pub struct ListAuditLogsRequest {
size: Option<usize>,
sort_ascending: Option<bool>,
}

#[derive(Debug, Serialize)]
Expand All @@ -44,6 +45,7 @@ pub async fn list_audit_logs(
&ctx,
state.audit_database_context(),
request.size.unwrap_or(0),
request.sort_ascending.unwrap_or(false),
)
.await?;

Expand Down
Loading

0 comments on commit 5797631

Please sign in to comment.