Merge branch 'feature/audit-logs' of github.com:Budibase/budibase into feature/audit-logs
This commit is contained in:
commit
49d2dc20d1
|
@ -32,7 +32,6 @@
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let fetchTerm = null
|
export let fetchTerm = null
|
||||||
$: console.log(fieldText)
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{dayjs(row.date).fromNow()}
|
||||||
|
</div>
|
|
@ -3,4 +3,4 @@
|
||||||
export let row
|
export let row
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Avatar size="M" initials={"PC"} />
|
<Avatar size="M" initials={row.user[0]} />
|
||||||
|
|
|
@ -2,26 +2,28 @@
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Table,
|
Table,
|
||||||
Select,
|
|
||||||
Search,
|
Search,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
notifications,
|
notifications,
|
||||||
|
Icon,
|
||||||
|
clickOutside,
|
||||||
|
CoreTextArea,
|
||||||
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { licensing, users, apps } from "stores/portal"
|
import { licensing, users, apps, auditLogs } from "stores/portal"
|
||||||
import LockedFeature from "../../_components/LockedFeature.svelte"
|
import LockedFeature from "../../_components/LockedFeature.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { getContext, setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import Portal from "svelte-portal"
|
|
||||||
import ViewDetailsRenderer from "./_components/ViewDetailsRenderer.svelte"
|
import ViewDetailsRenderer from "./_components/ViewDetailsRenderer.svelte"
|
||||||
import UserRenderer from "./_components/UserRenderer.svelte"
|
import UserRenderer from "./_components/UserRenderer.svelte"
|
||||||
|
import TimeRenderer from "./_components/TimeRenderer.svelte"
|
||||||
|
|
||||||
const sidePanel = getContext("side-panel")
|
|
||||||
const schema = {
|
const schema = {
|
||||||
name: {},
|
name: { width: "1fr" },
|
||||||
date: {},
|
date: { width: "1.5fr" },
|
||||||
user: { width: "auto" },
|
user: { width: "0.5fr" },
|
||||||
app: {},
|
app: { width: "1fr" },
|
||||||
event: {},
|
event: { width: "1fr" },
|
||||||
view: { width: "auto", borderLeft: true, displayName: "" },
|
view: { width: "auto", borderLeft: true, displayName: "" },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,18 +36,29 @@
|
||||||
column: "user",
|
column: "user",
|
||||||
component: UserRenderer,
|
component: UserRenderer,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
column: "date",
|
||||||
|
component: TimeRenderer,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let searchTerm = ""
|
let userSearchTerm = ""
|
||||||
let pageInfo = createPaginationStore()
|
let logSearchTerm = ""
|
||||||
let prevSearch = undefined
|
let userPageInfo = createPaginationStore()
|
||||||
|
let logsPageInfo = createPaginationStore()
|
||||||
|
|
||||||
|
let prevUserSearch = undefined
|
||||||
|
let prevLogSearch = undefined
|
||||||
let selectedUsers = []
|
let selectedUsers = []
|
||||||
|
let selectedApps = []
|
||||||
let selectedLog
|
let selectedLog
|
||||||
|
let sidePanelVisible = false
|
||||||
|
let startDate, endDate
|
||||||
|
|
||||||
let data = [
|
let data = [
|
||||||
{
|
{
|
||||||
name: "User created",
|
name: "User created",
|
||||||
date: "2021-03-01 12:00:00",
|
date: "2023-02-14T10:19:52.021Z",
|
||||||
user: "Peter Clement",
|
user: "Peter Clement",
|
||||||
app: "School Admin Panel",
|
app: "School Admin Panel",
|
||||||
event: "User added",
|
event: "User added",
|
||||||
|
@ -56,30 +69,61 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: fetchUsers(page, searchTerm)
|
$: fetchUsers(userPage, userSearchTerm)
|
||||||
$: page = $pageInfo.page
|
$: fetchLogs(logsPage, logSearchTerm)
|
||||||
|
|
||||||
|
$: userPage = $userPageInfo.page
|
||||||
|
$: logsPage = $logsPageInfo.page
|
||||||
|
|
||||||
$: enrichedList = enrich($users.data || [], selectedUsers)
|
$: enrichedList = enrich($users.data || [], selectedUsers)
|
||||||
$: sortedList = sort(enrichedList)
|
$: sortedList = sort(enrichedList)
|
||||||
|
|
||||||
const fetchUsers = async (page, search) => {
|
const fetchUsers = async (userPage, search) => {
|
||||||
if ($pageInfo.loading) {
|
if ($userPageInfo.loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// need to remove the page if they've started searching
|
// need to remove the page if they've started searching
|
||||||
if (search && !prevSearch) {
|
if (search && !prevUserSearch) {
|
||||||
pageInfo.reset()
|
userPageInfo.reset()
|
||||||
page = undefined
|
userPage = undefined
|
||||||
}
|
}
|
||||||
prevSearch = search
|
prevUserSearch = search
|
||||||
try {
|
try {
|
||||||
pageInfo.loading()
|
userPageInfo.loading()
|
||||||
await users.search({ page, email: search })
|
await users.search({ userPage, email: search })
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
userPageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error getting user list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchLogs = async (logsPage, search) => {
|
||||||
|
if ($logsPageInfo.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// need to remove the page if they've started searching
|
||||||
|
if (search && !prevLogSearch) {
|
||||||
|
logsPageInfo.reset()
|
||||||
|
logsPage = undefined
|
||||||
|
}
|
||||||
|
prevLogSearch = search
|
||||||
|
try {
|
||||||
|
logsPageInfo.loading()
|
||||||
|
await auditLogs.search({
|
||||||
|
logsPage,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
metadataSearch: search,
|
||||||
|
userIds: selectedUsers,
|
||||||
|
appIds: selectedApps,
|
||||||
|
})
|
||||||
|
logsPageInfo.fetched($auditLogs.hasNextPage, $auditLogs.nextPage)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
notifications.error("Error getting audit logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const enrich = (list, selected) => {
|
const enrich = (list, selected) => {
|
||||||
return list.map(item => {
|
return list.map(item => {
|
||||||
return {
|
return {
|
||||||
|
@ -106,7 +150,21 @@
|
||||||
|
|
||||||
const viewDetails = detail => {
|
const viewDetails = detail => {
|
||||||
selectedLog = detail
|
selectedLog = detail
|
||||||
sidePanel.open()
|
sidePanelVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadLogs = async () => {
|
||||||
|
try {
|
||||||
|
await auditLogs.download({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
metadataSearch: logSearchTerm,
|
||||||
|
userIds: selectedUsers,
|
||||||
|
appIds: selectedApps,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error downloading logs: ` + error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("auditLogs", {
|
setContext("auditLogs", {
|
||||||
|
@ -123,14 +181,25 @@
|
||||||
$licensing.goToUpgradePage()
|
$licensing.goToUpgradePage()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="datepicker" />
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<div class="select">
|
<div>
|
||||||
<Select placeholder="All" label="Activity" />
|
<DatePicker
|
||||||
|
range={true}
|
||||||
|
label="Date Range"
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail[0].length > 1) {
|
||||||
|
startDate = e.detail[0][0].toISOString()
|
||||||
|
endDate = e.detail[0][1].toISOString()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:fetchTerm={searchTerm}
|
bind:fetchTerm={userSearchTerm}
|
||||||
placeholder="All users"
|
placeholder="All users"
|
||||||
label="Users"
|
label="Users"
|
||||||
autocomplete
|
autocomplete
|
||||||
|
@ -147,13 +216,18 @@
|
||||||
getOptionValue={app => app.appId}
|
getOptionValue={app => app.appId}
|
||||||
getOptionLabel={app => app.name}
|
getOptionLabel={app => app.name}
|
||||||
options={$apps}
|
options={$apps}
|
||||||
|
bind:value={selectedApps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<Multiselect placeholder="All events" label="Event" />
|
<Multiselect placeholder="All events" label="Event" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 200px;">
|
<div style="padding-bottom: var(--spacing-s)">
|
||||||
|
<Icon on:click={() => downloadLogs()} name="Download" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-width: 150px; ">
|
||||||
<Search placeholder="Search" value={""} />
|
<Search placeholder="Search" value={""} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -171,30 +245,83 @@
|
||||||
</LockedFeature>
|
</LockedFeature>
|
||||||
|
|
||||||
{#if selectedLog}
|
{#if selectedLog}
|
||||||
<Portal target="#side-panel">
|
<div
|
||||||
<div>hello</div>
|
id="side-panel"
|
||||||
</Portal>
|
class:visible={sidePanelVisible}
|
||||||
|
use:clickOutside={() => {
|
||||||
|
sidePanelVisible = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="side-panel-header">
|
||||||
|
Audit Logs
|
||||||
|
<Icon
|
||||||
|
icon="Close"
|
||||||
|
on:click={() => {
|
||||||
|
sidePanelVisible = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="padding-top: 10px; height: 95%">
|
||||||
|
<CoreTextArea
|
||||||
|
disabled={true}
|
||||||
|
minHeight={"300px"}
|
||||||
|
height={"100%"}
|
||||||
|
value={JSON.stringify(selectedLog.metadata, null, 2)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.side-panel-header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--background);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
width: 320px;
|
||||||
|
max-width: calc(100vw - 48px - 48px);
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 130ms ease-in-out;
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
#side-panel.visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search :global(.spectrum-InputGroup) {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
align-items: flex-end;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-basis: 130px;
|
||||||
|
width: 0;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
}
|
gap: var(--spacing-xl);
|
||||||
.select {
|
align-items: flex-end;
|
||||||
flex: 1 1 0;
|
|
||||||
max-width: 200px;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
notifications.error(`Error saving variable: ${err.message}`)
|
notifications.error(`Error saving variable: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: console.log($environment.variables)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LockedFeature
|
<LockedFeature
|
||||||
|
|
|
@ -34,4 +34,4 @@ export function createAuditLogsStore() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const environment = createAuditLogsStore()
|
export const auditLogs = createAuditLogsStore()
|
||||||
|
|
|
@ -13,3 +13,4 @@ export { backups } from "./backups"
|
||||||
export { overview } from "./overview"
|
export { overview } from "./overview"
|
||||||
export { environment } from "./environment"
|
export { environment } from "./environment"
|
||||||
export { menu } from "./menu"
|
export { menu } from "./menu"
|
||||||
|
export { auditLogs } from "./auditLogs"
|
||||||
|
|
|
@ -38,20 +38,20 @@ export const buildAuditLogsEndpoints = API => ({
|
||||||
*/
|
*/
|
||||||
searchAuditLogs: async opts => {
|
searchAuditLogs: async opts => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/auditlogs/search`,
|
url: `/api/global/auditlogs/search`,
|
||||||
body: buildOpts(opts),
|
body: buildOpts(opts),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventDefinitions: async () => {
|
getEventDefinitions: async () => {
|
||||||
return await API.get({
|
return await API.get({
|
||||||
url: `/api/auditlogs/definitions`,
|
url: `/api/global/auditlogs/definitions`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadLogs: async opts => {
|
downloadLogs: async opts => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/auditlogs/definitions`,
|
url: `/api/global/auditlogs/definitions`,
|
||||||
body: buildOpts(opts),
|
body: buildOpts(opts),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue