commit
2e3698a919
|
@ -17,7 +17,7 @@
|
|||
export let timeOnly = false
|
||||
export let ignoreTimezones = false
|
||||
export let time24hr = false
|
||||
|
||||
export let range = false
|
||||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
let open = false
|
||||
|
@ -41,6 +41,7 @@
|
|||
time_24hr: time24hr || false,
|
||||
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||
wrap: true,
|
||||
mode: range ? "range" : null,
|
||||
appendTo,
|
||||
disableMobile: "true",
|
||||
onReady: () => {
|
||||
|
@ -64,9 +65,8 @@
|
|||
if (newValue) {
|
||||
newValue = newValue.toISOString()
|
||||
}
|
||||
|
||||
// If time only set date component to 2000-01-01
|
||||
if (timeOnly) {
|
||||
else if (timeOnly) {
|
||||
// Classic flackpickr causing issues.
|
||||
// When selecting a value for the first time for a "time only" field,
|
||||
// the time is always offset by 1 hour for some reason (regardless of time
|
||||
|
@ -95,7 +95,11 @@
|
|||
.slice(0, -1)
|
||||
}
|
||||
|
||||
dispatch("change", newValue)
|
||||
if (range) {
|
||||
dispatch("change", event.detail)
|
||||
} else {
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
}
|
||||
|
||||
const clearDateOnBackspace = event => {
|
||||
|
@ -160,7 +164,7 @@
|
|||
{#key redrawOptions}
|
||||
<Flatpickr
|
||||
bind:flatpickr
|
||||
value={parseDate(value)}
|
||||
value={range ? value : parseDate(value)}
|
||||
on:open={onOpen}
|
||||
on:close={onClose}
|
||||
options={flatpickrOptions}
|
||||
|
|
|
@ -14,11 +14,17 @@
|
|||
export let placeholder = null
|
||||
export let appendTo = undefined
|
||||
export let ignoreTimezones = false
|
||||
|
||||
export let range = false
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
if (range) {
|
||||
// Flatpickr cant take two dates and work out what to display, needs to be provided a string.
|
||||
// Like - "Date1 to Date2". Hence passing in that specifically from the array
|
||||
value = e?.detail[1]
|
||||
} else {
|
||||
value = e.detail
|
||||
}
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
</script>
|
||||
|
@ -34,6 +40,7 @@
|
|||
{time24hr}
|
||||
{appendTo}
|
||||
{ignoreTimezones}
|
||||
{range}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
{schema}
|
||||
value={cellValue}
|
||||
on:clickrelationship
|
||||
on:buttonclick
|
||||
>
|
||||
<slot />
|
||||
</svelte:component>
|
||||
|
|
|
@ -387,6 +387,7 @@
|
|||
schema={schema[field]}
|
||||
value={deepGet(row, field)}
|
||||
on:clickrelationship
|
||||
on:buttonclick
|
||||
>
|
||||
<slot />
|
||||
</CellRenderer>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 314 KiB |
|
@ -0,0 +1,114 @@
|
|||
<script>
|
||||
import {
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Input,
|
||||
Heading,
|
||||
Body,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let row
|
||||
|
||||
let deleteDialog
|
||||
let restoreDialog
|
||||
let updateDialog
|
||||
let name
|
||||
let restoreBackupModal
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onClickRestore = name => {
|
||||
dispatch("buttonclick", {
|
||||
type: "backupRestore",
|
||||
name,
|
||||
backupId: row._id,
|
||||
restoreBackupName: name,
|
||||
})
|
||||
}
|
||||
|
||||
const onClickDelete = () => {
|
||||
dispatch("buttonclick", {
|
||||
type: "backupDelete",
|
||||
backupId: row._id,
|
||||
})
|
||||
}
|
||||
|
||||
const onClickUpdate = () => {
|
||||
dispatch("buttonclick", {
|
||||
type: "backupUpdate",
|
||||
backupId: row._id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadExport() {
|
||||
window.open(`/api/apps/${row.appId}/backups/${row._id}/file`, "_blank")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
<ActionMenu align="right">
|
||||
<div slot="control">
|
||||
<Icon size="M" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
|
||||
{#if row.type !== "restore"}
|
||||
<MenuItem on:click={restoreDialog.show} icon="Revert">Restore</MenuItem>
|
||||
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
||||
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
||||
{/if}
|
||||
<MenuItem on:click={updateDialog.show} icon="Edit">Update</MenuItem>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={restoreBackupModal}>
|
||||
<CreateRestoreModal confirm={name => onClickRestore(name)} />
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={deleteDialog}
|
||||
okText="Delete Backup"
|
||||
onOk={onClickDelete}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you wish to delete the backup
|
||||
<i>{row.name}</i>
|
||||
This action cannot be undone.
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={restoreDialog}
|
||||
okText="Continue"
|
||||
onOk={restoreBackupModal?.show}
|
||||
title="Confirm restore"
|
||||
warning={false}
|
||||
>
|
||||
<Heading size="S">{row.name || "Backup"}</Heading>
|
||||
<Body size="S">{new Date(row.timestamp).toLocaleString()}</Body>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={updateDialog}
|
||||
disabled={!name}
|
||||
okText="Confirm"
|
||||
onOk={onClickUpdate}
|
||||
title="Update Backup"
|
||||
warning={false}
|
||||
>
|
||||
<Input onlabel="Backup name" placeholder={row.name} bind:value={name} />
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let row
|
||||
|
||||
$: automations = row?.automations
|
||||
$: datasources = row?.datasources
|
||||
$: screens = row?.screens
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
{#if automations != null && screens != null && datasources != null}
|
||||
<div class="item">
|
||||
<Icon name="Data" />
|
||||
<div>{datasources || 0}</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<Icon name="WebPage" />
|
||||
<div>{screens || 0}</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<Icon name="JourneyVoyager" />
|
||||
<div>{automations || 0}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc(var(--spacing-xl) * 2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,336 @@
|
|||
<script>
|
||||
import {
|
||||
ActionButton,
|
||||
Button,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Layout,
|
||||
Modal,
|
||||
notifications,
|
||||
Pagination,
|
||||
Select,
|
||||
Heading,
|
||||
Body,
|
||||
Tags,
|
||||
Tag,
|
||||
Table,
|
||||
Page,
|
||||
} from "@budibase/bbui"
|
||||
import { backups, licensing, auth, admin } from "stores/portal"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import AppSizeRenderer from "./AppSizeRenderer.svelte"
|
||||
import CreateBackupModal from "./CreateBackupModal.svelte"
|
||||
import ActionsRenderer from "./ActionsRenderer.svelte"
|
||||
import DateRenderer from "./DateRenderer.svelte"
|
||||
import UserRenderer from "./UserRenderer.svelte"
|
||||
import StatusRenderer from "./StatusRenderer.svelte"
|
||||
import TypeRenderer from "./TypeRenderer.svelte"
|
||||
import BackupsDefault from "assets/backups-default.png"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let app
|
||||
|
||||
let backupData = null
|
||||
let modal
|
||||
let pageInfo = createPaginationStore()
|
||||
let filterOpt = null
|
||||
let startDate = null
|
||||
let endDate = null
|
||||
let filters = getFilters()
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fetchBackups(filterOpt, page, startDate, endDate)
|
||||
|
||||
function getFilters() {
|
||||
const options = []
|
||||
let types = ["backup"]
|
||||
let triggers = ["manual", "publish", "scheduled", "restoring"]
|
||||
for (let type of types) {
|
||||
for (let trigger of triggers) {
|
||||
let label = `${trigger} ${type}`
|
||||
label = label.charAt(0).toUpperCase() + label?.slice(1)
|
||||
options.push({ label, value: { type, trigger } })
|
||||
}
|
||||
}
|
||||
options.push({
|
||||
label: `Manual restore`,
|
||||
value: { type: "restore", trigger: "manual" },
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
const schema = {
|
||||
type: {
|
||||
displayName: "Type",
|
||||
},
|
||||
createdAt: {
|
||||
displayName: "Date",
|
||||
},
|
||||
name: {
|
||||
displayName: "Name",
|
||||
},
|
||||
appSize: {
|
||||
displayName: "App size",
|
||||
},
|
||||
createdBy: {
|
||||
displayName: "User",
|
||||
},
|
||||
status: {
|
||||
displayName: "Status",
|
||||
},
|
||||
actions: {
|
||||
displayName: null,
|
||||
},
|
||||
}
|
||||
|
||||
const customRenderers = [
|
||||
{ column: "appSize", component: AppSizeRenderer },
|
||||
{ column: "actions", component: ActionsRenderer },
|
||||
{ column: "createdAt", component: DateRenderer },
|
||||
{ column: "createdBy", component: UserRenderer },
|
||||
{ column: "status", component: StatusRenderer },
|
||||
{ column: "type", component: TypeRenderer },
|
||||
]
|
||||
|
||||
function flattenBackups(backups) {
|
||||
return backups.map(backup => {
|
||||
return {
|
||||
...backup,
|
||||
...backup?.contents,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchBackups(filters, page, startDate, endDate) {
|
||||
const response = await backups.searchBackups({
|
||||
appId: app.instance._id,
|
||||
...filters,
|
||||
page,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
||||
|
||||
// flatten so we have an easier structure to use for the table schema
|
||||
backupData = flattenBackups(response.data)
|
||||
}
|
||||
|
||||
async function createManualBackup(name) {
|
||||
try {
|
||||
let response = await backups.createManualBackup({
|
||||
appId: app.instance._id,
|
||||
name,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
notifications.success(response.message)
|
||||
} catch {
|
||||
notifications.error("Unable to create backup")
|
||||
}
|
||||
}
|
||||
|
||||
async function handleButtonClick({ detail }) {
|
||||
if (detail.type === "backupDelete") {
|
||||
await backups.deleteBackup({
|
||||
appId: app.instance._id,
|
||||
backupId: detail.backupId,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
} else if (detail.type === "backupRestore") {
|
||||
await backups.restoreBackup({
|
||||
appId: app.instance._id,
|
||||
backupId: detail.backupId,
|
||||
name: detail.restoreBackupName,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
} else if (detail.type === "backupUpdate") {
|
||||
await backups.updateBackup({
|
||||
appId: app.instance._id,
|
||||
backupId: detail.backupId,
|
||||
name: detail.name,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchBackups(filterOpt, page, startDate, endDate)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
{#if !$licensing.backupsEnabled}
|
||||
<Page wide={false}>
|
||||
<Layout gap="XS" noPadding>
|
||||
<div class="title">
|
||||
<Heading size="M">Backups</Heading>
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
<div>
|
||||
<Body>
|
||||
Backup your apps and restore them to their previous state.
|
||||
{#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud}
|
||||
Contact your account holder to upgrade your plan.
|
||||
{/if}
|
||||
</Body>
|
||||
</div>
|
||||
<Divider />
|
||||
<div class="pro-buttons">
|
||||
{#if $auth.accountPortalAccess}
|
||||
<Button
|
||||
newStyles
|
||||
primary
|
||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||
on:click={$licensing.goToUpgradePage()}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
{/if}
|
||||
<!--Show the view plans button-->
|
||||
<Button
|
||||
newStyles
|
||||
secondary
|
||||
on:click={() => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}}
|
||||
>
|
||||
View Plans
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</Page>
|
||||
{:else if backupData?.length > 0}
|
||||
<Layout noPadding gap="M" alignContent="start">
|
||||
<div class="search">
|
||||
<div class="select">
|
||||
<Select
|
||||
placeholder="All"
|
||||
label="Type"
|
||||
options={filters}
|
||||
getOptionValue={filter => filter.value}
|
||||
getOptionLabel={filter => filter.label}
|
||||
bind:value={filterOpt}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<DatePicker
|
||||
range={true}
|
||||
label={"Filter Range"}
|
||||
on:change={e => {
|
||||
if (e.detail[0].length > 1) {
|
||||
startDate = e.detail[0][0].toISOString()
|
||||
endDate = e.detail[0][1].toISOString()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="split-buttons">
|
||||
<ActionButton on:click={modal.show} icon="SaveAsFloppy"
|
||||
>Create new backup</ActionButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Table
|
||||
{schema}
|
||||
allowSelectRows={false}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
data={backupData}
|
||||
{customRenderers}
|
||||
placeholderText="No backups found"
|
||||
border={false}
|
||||
on:buttonclick={handleButtonClick}
|
||||
/>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$pageInfo.pageNumber}
|
||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||
goToPrevPage={pageInfo.prevPage}
|
||||
goToNextPage={pageInfo.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
{:else if backupData?.length === 0}
|
||||
<Page wide={false}>
|
||||
<div class="align">
|
||||
<img
|
||||
width="200px"
|
||||
height="120px"
|
||||
src={BackupsDefault}
|
||||
alt="BackupsDefault"
|
||||
/>
|
||||
<Layout gap="S">
|
||||
<Heading>You have no backups yet</Heading>
|
||||
<div class="opacity">
|
||||
<Body size="S">You can manually backup your app any time</Body>
|
||||
</div>
|
||||
<div class="padding">
|
||||
<Button on:click={modal.show} cta>Create Backup</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</Page>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateBackupModal {createManualBackup} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
padding: var(--spectrum-alias-grid-gutter-medium)
|
||||
var(--spectrum-alias-grid-gutter-large);
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: var(--spacing-xl);
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-basis: 150px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.split-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.align {
|
||||
margin-top: 5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pro-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { ModalContent, Input } from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let createManualBackup
|
||||
|
||||
let templateName = $auth.user.firstName
|
||||
? `${$auth.user.firstName}'s Backup`
|
||||
: "New Backup"
|
||||
let name = templateName
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={() => createManualBackup(name)}
|
||||
title="Create new backup"
|
||||
confirmText="Create"
|
||||
><Input label="Backup name" bind:value={name} /></ModalContent
|
||||
>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import { ModalContent, Input, Body } from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let confirm
|
||||
|
||||
let templateName = $auth.user.firstName
|
||||
? `${$auth.user.firstName}'s Backup`
|
||||
: "Restore Backup"
|
||||
let name = templateName
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={() => confirm(name)}
|
||||
title="Backup your current version"
|
||||
confirmText="Confirm Restore"
|
||||
disabled={!name}
|
||||
>
|
||||
<Body size="S"
|
||||
>Create a backup of your current app to allow you to roll back after
|
||||
restoring this backup</Body
|
||||
>
|
||||
<Input label="Backup name" bind:value={name} />
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
{#if value != null}
|
||||
<Icon name="Data" />
|
||||
<div>{value || 0}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export let value
|
||||
$: timeSince = dayjs(value).fromNow()
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
{timeSince} - <DateTimeRenderer {value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { Badge } from "@budibase/bbui"
|
||||
|
||||
export let value = "started"
|
||||
$: status = value[0].toUpperCase() + value?.slice(1)
|
||||
</script>
|
||||
|
||||
<Badge
|
||||
grey={value === "started" || value === "pending"}
|
||||
green={value === "complete"}
|
||||
red={value === "failed"}
|
||||
size="S"
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
export let row
|
||||
|
||||
$: baseTrig = row?.trigger || "manual"
|
||||
$: type = row?.type || "backup"
|
||||
$: trigger = baseTrig.charAt(0).toUpperCase() + baseTrig.slice(1)
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
{trigger}
|
||||
{type}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
export let value
|
||||
|
||||
let firstName = value?.firstName
|
||||
let lastName = value?.lastName || ""
|
||||
|
||||
$: username =
|
||||
firstName && lastName ? `${firstName} ${lastName}` : value?.email
|
||||
</script>
|
||||
|
||||
<div class="cell">
|
||||
{#if value != null}
|
||||
<div>{username}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -113,18 +113,23 @@
|
|||
>
|
||||
Access
|
||||
</MenuItem>
|
||||
{#if isPublished}
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(
|
||||
`../../portal/overview/${application}?tab=${encodeURIComponent(
|
||||
"Automation History"
|
||||
)}`
|
||||
)}
|
||||
>
|
||||
Automation history
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(
|
||||
`../../portal/overview/${application}?tab=${encodeURIComponent(
|
||||
"Automation History"
|
||||
)}`
|
||||
)}
|
||||
>
|
||||
Automation history
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}?tab=Backups`)}
|
||||
>
|
||||
Backups
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}?tab=Settings`)}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import BackupsTab from "components/portal/overview/backups/BackupsTab.svelte"
|
||||
|
||||
export let application
|
||||
|
||||
|
@ -318,16 +319,12 @@
|
|||
<Tab title="Access">
|
||||
<AccessTab app={selectedApp} />
|
||||
</Tab>
|
||||
{#if isPublished}
|
||||
<Tab title="Automation History">
|
||||
<HistoryTab app={selectedApp} />
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if false}
|
||||
<Tab title="Backups">
|
||||
<div class="container">Backups contents</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
<Tab title="Automation History">
|
||||
<HistoryTab app={selectedApp} />
|
||||
</Tab>
|
||||
<Tab title="Backups">
|
||||
<BackupsTab app={selectedApp} />
|
||||
</Tab>
|
||||
<Tab title="Settings">
|
||||
<SettingsTab app={selectedApp} />
|
||||
</Tab>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "api"
|
||||
|
||||
export function createBackupsStore() {
|
||||
const store = writable({})
|
||||
|
||||
function selectBackup(backupId) {
|
||||
store.update(state => {
|
||||
state.selectedBackup = backupId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async function searchBackups({
|
||||
appId,
|
||||
trigger,
|
||||
type,
|
||||
page,
|
||||
startDate,
|
||||
endDate,
|
||||
}) {
|
||||
return API.searchBackups({ appId, trigger, type, page, startDate, endDate })
|
||||
}
|
||||
|
||||
async function restoreBackup({ appId, backupId, name }) {
|
||||
return API.restoreBackup({ appId, backupId, name })
|
||||
}
|
||||
|
||||
async function deleteBackup({ appId, backupId }) {
|
||||
return API.deleteBackup({ appId, backupId })
|
||||
}
|
||||
|
||||
async function createManualBackup(appId, name) {
|
||||
return API.createManualBackup(appId, name)
|
||||
}
|
||||
|
||||
async function updateBackup({ appId, backupId, name }) {
|
||||
return API.updateBackup({ appId, backupId, name })
|
||||
}
|
||||
|
||||
return {
|
||||
createManualBackup,
|
||||
searchBackups,
|
||||
selectBackup,
|
||||
deleteBackup,
|
||||
restoreBackup,
|
||||
updateBackup,
|
||||
subscribe: store.subscribe,
|
||||
}
|
||||
}
|
||||
|
||||
export const backups = createBackupsStore()
|
|
@ -9,3 +9,4 @@ export { templates } from "./templates"
|
|||
export { licensing } from "./licensing"
|
||||
export { groups } from "./groups"
|
||||
export { plugins } from "./plugins"
|
||||
export { backups } from "./backups"
|
||||
|
|
|
@ -14,6 +14,7 @@ export const createLicensingStore = () => {
|
|||
isFreePlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
backupsEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
|
@ -56,12 +57,17 @@ export const createLicensingStore = () => {
|
|||
const groupsEnabled = license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
const backupsEnabled = license.features.includes(
|
||||
Constants.Features.BACKUPS
|
||||
)
|
||||
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
license,
|
||||
isFreePlan,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
export const buildBackupsEndpoints = API => ({
|
||||
/**
|
||||
* Gets a list of users in the current tenant.
|
||||
*/
|
||||
searchBackups: async ({ appId, trigger, type, page, startDate, endDate }) => {
|
||||
const opts = {}
|
||||
if (page) {
|
||||
opts.page = page
|
||||
}
|
||||
if (trigger && type) {
|
||||
opts.trigger = trigger.toLowerCase()
|
||||
opts.type = type.toLowerCase()
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
opts.startDate = startDate
|
||||
opts.endDate = endDate
|
||||
}
|
||||
return await API.post({
|
||||
url: `/api/apps/${appId}/backups/search`,
|
||||
body: opts,
|
||||
})
|
||||
},
|
||||
|
||||
createManualBackup: async ({ appId, name }) => {
|
||||
return await API.post({
|
||||
url: `/api/apps/${appId}/backups`,
|
||||
body: { name },
|
||||
})
|
||||
},
|
||||
|
||||
deleteBackup: async ({ appId, backupId }) => {
|
||||
return await API.delete({
|
||||
url: `/api/apps/${appId}/backups/${backupId}`,
|
||||
})
|
||||
},
|
||||
|
||||
updateBackup: async ({ appId, backupId, name }) => {
|
||||
return await API.patch({
|
||||
url: `/api/apps/${appId}/backups/${backupId}`,
|
||||
body: { name },
|
||||
})
|
||||
},
|
||||
|
||||
restoreBackup: async ({ appId, backupId, name }) => {
|
||||
return await API.post({
|
||||
url: `/api/apps/${appId}/backups/${backupId}/import`,
|
||||
body: { name },
|
||||
})
|
||||
},
|
||||
})
|
|
@ -25,6 +25,7 @@ import { buildViewEndpoints } from "./views"
|
|||
import { buildLicensingEndpoints } from "./licensing"
|
||||
import { buildGroupsEndpoints } from "./groups"
|
||||
import { buildPluginEndpoints } from "./plugins"
|
||||
import { buildBackupsEndpoints } from "./backups"
|
||||
|
||||
const defaultAPIClientConfig = {
|
||||
/**
|
||||
|
@ -245,5 +246,6 @@ export const createAPIClient = config => {
|
|||
...buildLicensingEndpoints(API),
|
||||
...buildGroupsEndpoints(API),
|
||||
...buildPluginEndpoints(API),
|
||||
...buildBackupsEndpoints(API),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ export const ApiVersion = "1"
|
|||
|
||||
export const Features = {
|
||||
USER_GROUPS: "userGroups",
|
||||
BACKUPS: "appBackups",
|
||||
}
|
||||
|
||||
// Role IDs
|
||||
|
|
Loading…
Reference in New Issue