1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 18:32:35 +00:00

Refactor UI for workspace applications export

This commit is contained in:
Przemyslaw Kukulski 2024-10-15 08:53:16 +00:00
parent 6751a676ed
commit 685e8240cd
18 changed files with 539 additions and 100 deletions

View file

@ -5,6 +5,7 @@ from .users import urls as user_urls
from .views import (
AsyncExportWorkspaceApplicationsView,
CreateInitialWorkspaceView,
ListExportWorkspaceApplicationsView,
WorkspaceGenerativeAISettingsView,
WorkspaceLeaveView,
WorkspaceOrderView,
@ -44,4 +45,9 @@ urlpatterns = [
AsyncExportWorkspaceApplicationsView.as_view(),
name="export_workspace_async",
),
re_path(
r"(?P<workspace_id>[0-9]+)/export/$",
ListExportWorkspaceApplicationsView.as_view(),
name="export_workspace_list",
),
]

View file

@ -44,6 +44,7 @@ from baserow.core.exceptions import (
)
from baserow.core.feature_flags import FF_EXPORT_WORKSPACE, feature_flag_is_enabled
from baserow.core.handler import CoreHandler
from baserow.core.import_export_handler import ImportExportHandler
from baserow.core.job_types import ExportApplicationsJobType
from baserow.core.jobs.exceptions import MaxJobCountExceeded
from baserow.core.jobs.handler import JobHandler
@ -71,11 +72,15 @@ ExportApplicationsJobRequestSerializer = job_type_registry.get(
ExportApplicationsJobResponseSerializer = job_type_registry.get(
ExportApplicationsJobType.type
).get_serializer_class(
base_class=serializers.Serializer,
base_class=JobSerializer,
meta_ref_name="SingleExportApplicationsJobRequestSerializer",
)
class ListExportWorkspaceApplicationsSerializer(serializers.Serializer):
results = ExportApplicationsJobResponseSerializer(many=True)
class WorkspacesView(APIView):
permission_classes = (IsAuthenticated,)
@ -471,6 +476,47 @@ class CreateInitialWorkspaceView(APIView):
return Response(WorkspaceUserWorkspaceSerializer(workspace_user).data)
class ListExportWorkspaceApplicationsView(APIView):
permission_classes = (IsAuthenticated,)
@extend_schema(
parameters=[
OpenApiParameter(
name="workspace_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The id of the workspace that is being exported.",
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Workspaces"],
operation_id="list_workspace_exports",
description="Lists exports that were created for given workspace.",
responses={
200: ListExportWorkspaceApplicationsSerializer,
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
},
)
@map_exceptions(
{
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
}
)
def get(self, request, workspace_id):
"""
Lists all available exports created for a given workspace.
"""
feature_flag_is_enabled(FF_EXPORT_WORKSPACE, raise_if_disabled=True)
exports = ImportExportHandler().list(workspace_id, request.user)
return Response(
ListExportWorkspaceApplicationsSerializer({"results": exports}).data
)
class AsyncExportWorkspaceApplicationsView(APIView):
permission_classes = (IsAuthenticated,)
@ -484,7 +530,7 @@ class AsyncExportWorkspaceApplicationsView(APIView):
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Workspace"],
tags=["Workspaces"],
operation_id="export_workspace_applications_async",
description=(
"Export workspace or set of applications application if the authorized user is "

View file

@ -5,12 +5,17 @@ from typing import Dict, List, Optional
from zipfile import ZIP_DEFLATED, ZipFile
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.files.base import ContentFile
from django.core.files.storage import Storage
from django.db.models import QuerySet
from opentelemetry import trace
from baserow.core.models import Application, Workspace
from baserow.core.handler import CoreHandler
from baserow.core.jobs.constants import JOB_FINISHED
from baserow.core.models import Application, ExportApplicationsJob, Workspace
from baserow.core.operations import ReadWorkspaceOperationType
from baserow.core.registries import ImportExportConfig, application_type_registry
from baserow.core.storage import (
_create_storage_dir_if_missing_and_open,
@ -21,6 +26,8 @@ from baserow.core.utils import ChildProgressBuilder, Progress
tracer = trace.get_tracer(__name__)
WORKSPACE_EXPORTS_LIMIT = 5
class ImportExportHandler(metaclass=baserow_trace_methods(tracer)):
def export_application(
@ -167,3 +174,34 @@ class ImportExportHandler(metaclass=baserow_trace_methods(tracer)):
)
progress.increment(by=20)
return zip_file_name
def list(self, workspace_id: int, performed_by: AbstractUser) -> QuerySet:
"""
Lists all workspace application exports for the given workspace id
if the provided user is in the same workspace.
:param workspace_id: The workspace ID of which the applications are exported.
:param performed_by: The user performing the operation that should
have sufficient permissions.
:return: A queryset for workspace export jobs that were created for the given
workspace.
"""
workspace = CoreHandler().get_workspace(workspace_id)
CoreHandler().check_permissions(
performed_by,
ReadWorkspaceOperationType.type,
workspace=workspace,
context=workspace,
)
return (
ExportApplicationsJob.objects.filter(
workspace_id=workspace_id,
state=JOB_FINISHED,
user=performed_by,
)
.select_related("user")
.order_by("-updated_on", "-id")[:WORKSPACE_EXPORTS_LIMIT]
)

View file

@ -219,7 +219,11 @@ class ExportApplicationsJobType(JobType):
job_exceptions_map = {PermissionDenied: ERROR_PERMISSION_DENIED}
request_serializer_field_names = ["workspace_id", "application_ids"]
request_serializer_field_names = [
"workspace_id",
"application_ids",
"only_structure",
]
request_serializer_field_overrides = {
"application_ids": serializers.ListField(
allow_null=True,
@ -242,7 +246,7 @@ class ExportApplicationsJobType(JobType):
}
serializer_mixins = [ExportWorkspaceExportedFileURLSerializerMixin]
serializer_field_names = ["exported_file_name", "url"]
serializer_field_names = ["exported_file_name", "url", "created_on", "workspace_id"]
def transaction_atomic_context(self, job: "DuplicateApplicationJob"):
"""

View file

@ -5,7 +5,9 @@ from django.test.utils import override_settings
from django.urls import reverse
import pytest
from freezegun import freeze_time
from rest_framework.status import (
HTTP_200_OK,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
@ -118,8 +120,9 @@ def test_exporting_empty_workspace(
user = data_fixture.create_user()
workspace = data_fixture.create_workspace(user=user)
token = data_fixture.generate_token(user)
with django_capture_on_commit_callbacks(execute=True):
run_time = "2024-10-14T08:00:00Z"
with django_capture_on_commit_callbacks(execute=True), freeze_time(run_time):
token = data_fixture.generate_token(user)
response = api_client.post(
reverse(
"api:workspaces:export_workspace_async",
@ -135,6 +138,7 @@ def test_exporting_empty_workspace(
job_id = response_json["id"]
assert response_json == {
"created_on": run_time,
"exported_file_name": "",
"human_readable_error": "",
"id": job_id,
@ -142,13 +146,16 @@ def test_exporting_empty_workspace(
"state": "pending",
"type": "export_applications",
"url": None,
"workspace_id": workspace.id,
}
token = data_fixture.generate_token(user)
response = api_client.get(
reverse("api:jobs:item", kwargs={"job_id": job_id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
file_name = response_json["exported_file_name"]
@ -182,8 +189,9 @@ def test_exporting_workspace_with_single_empty_database(
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
token = data_fixture.generate_token(user)
with django_capture_on_commit_callbacks(execute=True):
run_time = "2024-10-14T08:00:00Z"
with django_capture_on_commit_callbacks(execute=True), freeze_time(run_time):
token = data_fixture.generate_token(user)
response = api_client.post(
reverse(
"api:workspaces:export_workspace_async",
@ -199,6 +207,7 @@ def test_exporting_workspace_with_single_empty_database(
job_id = response_json["id"]
assert response_json == {
"created_on": run_time,
"exported_file_name": "",
"human_readable_error": "",
"id": job_id,
@ -206,13 +215,16 @@ def test_exporting_workspace_with_single_empty_database(
"state": "pending",
"type": "export_applications",
"url": None,
"workspace_id": database.workspace.id,
}
token = data_fixture.generate_token(user)
response = api_client.get(
reverse("api:jobs:item", kwargs={"job_id": job_id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
file_name = response_json["exported_file_name"]
@ -241,3 +253,137 @@ def test_exporting_workspace_with_single_empty_database(
"tables": [],
}
]
@pytest.mark.django_db
@override_settings(
FEATURE_FLAGS="",
)
def test_list_exports_with_feature_flag_disabled(data_fixture, api_client, tmpdir):
user, token = data_fixture.create_user_and_token()
workspace = data_fixture.create_workspace(user=user)
data_fixture.create_database_application(workspace=workspace)
response = api_client.get(
reverse(
"api:workspaces:export_workspace_list",
kwargs={"workspace_id": workspace.id},
),
data={},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_403_FORBIDDEN
assert response.json()["error"] == "ERROR_FEATURE_DISABLED"
@pytest.mark.django_db
def test_list_exports_with_missing_workspace_returns_error(
data_fixture, api_client, tmpdir
):
user, token = data_fixture.create_user_and_token()
workspace = data_fixture.create_workspace(user=user)
data_fixture.create_database_application(workspace=workspace)
response = api_client.get(
reverse(
"api:workspaces:export_workspace_list",
kwargs={"workspace_id": 9999},
),
data={},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_GROUP_DOES_NOT_EXIST"
@pytest.mark.django_db(transaction=True)
def test_list_exports_for_invalid_user(
data_fixture,
api_client,
tmpdir,
django_capture_on_commit_callbacks,
use_tmp_media_root,
):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
user2 = data_fixture.create_user()
run_time = "2024-10-14T08:00:00Z"
with django_capture_on_commit_callbacks(execute=True), freeze_time(run_time):
token = data_fixture.generate_token(user)
api_client.post(
reverse(
"api:workspaces:export_workspace_async",
kwargs={"workspace_id": database.workspace.id},
),
data={
"application_ids": [],
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
token2 = data_fixture.generate_token(user2)
response = api_client.get(
reverse(
"api:workspaces:export_workspace_list",
kwargs={"workspace_id": database.workspace.id},
),
data={},
format="json",
HTTP_AUTHORIZATION=f"JWT {token2}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
@pytest.mark.django_db(transaction=True)
def test_list_exports_for_valid_user(
data_fixture,
api_client,
tmpdir,
django_capture_on_commit_callbacks,
use_tmp_media_root,
):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
run_time = "2024-10-14T08:00:00Z"
with django_capture_on_commit_callbacks(execute=True), freeze_time(run_time):
token = data_fixture.generate_token(user)
api_client.post(
reverse(
"api:workspaces:export_workspace_async",
kwargs={"workspace_id": database.workspace.id},
),
data={
"application_ids": [],
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
token = data_fixture.generate_token(user)
response = api_client.get(
reverse(
"api:workspaces:export_workspace_list",
kwargs={"workspace_id": database.workspace.id},
),
data={},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert "results" in response_json
assert len(response_json["results"]) == 1
export = response_json["results"][0]
file_name = export["exported_file_name"]
assert export["state"] == "finished"
assert export["progress_percentage"] == 100
assert export["url"] == f"http://localhost:8000/media/export_files/{file_name}"

View file

@ -4,6 +4,8 @@ import zipfile
from django.urls import reverse
import pytest
from freezegun import freeze_time
from rest_framework.status import HTTP_200_OK
from baserow.contrib.database.rows.handler import RowHandler
from baserow.core.import_export_handler import ImportExportHandler
@ -85,23 +87,26 @@ def test_exporting_workspace_writes_file_to_storage(
},
)
token = data_fixture.generate_token(user)
with django_capture_on_commit_callbacks(execute=True):
response = api_client.post(
reverse(
"api:workspaces:export_workspace_async",
kwargs={"workspace_id": table.database.workspace.id},
),
data={
"application_ids": [],
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
run_time = "2024-10-14T08:00:00Z"
with freeze_time(run_time):
token = data_fixture.generate_token(user)
with django_capture_on_commit_callbacks(execute=True):
response = api_client.post(
reverse(
"api:workspaces:export_workspace_async",
kwargs={"workspace_id": table.database.workspace.id},
),
data={
"application_ids": [],
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
job_id = response_json["id"]
assert response_json == {
"created_on": run_time,
"exported_file_name": "",
"human_readable_error": "",
"id": job_id,
@ -109,14 +114,17 @@ def test_exporting_workspace_writes_file_to_storage(
"state": "pending",
"type": "export_applications",
"url": None,
"workspace_id": table.database.workspace.id,
}
token = data_fixture.generate_token(user)
response = api_client.get(
reverse("api:jobs:item", kwargs={"job_id": job_id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
file_name = response_json["exported_file_name"]

View file

@ -123,6 +123,7 @@
@import 'preview';
@import 'deactivated_label';
@import 'snapshots_modal';
@import 'exports_modal';
@import 'import_modal';
@import 'row_edit_modal';
@import 'row_edit_modal_sidebar';

View file

@ -0,0 +1,44 @@
.exports-modal__name-input {
margin-right: 20px;
}
.exports-modal__export {
display: flex;
justify-content: space-between;
align-items: center;
&:not(:last-child) {
margin-bottom: 20px;
}
}
.exports-modal__info {
min-height: 36px;
flex-grow: 1;
display: flex;
align-items: center;
}
.exports-modal__list {
border-top: 1px solid #d9dbde;
padding-top: 30px;
margin-top: 30px;
}
.exports-modal__list--loading {
margin: auto;
}
.exports-modal__name {
font-weight: 600;
padding-bottom: 5px;
}
.exports-modal__detail {
color: $color-neutral-500;
}
.exports-modal__actions a {
display: inline-block;
margin-left: 20px;
}

View file

@ -1,7 +1,7 @@
<template>
<form @submit.prevent="submit">
<FormGroup :error="fieldHasErrors('name')" small-label required>
<slot name="settings"> </slot>
<slot name="select-applications"> </slot>
<template #after-input>
<slot></slot>

View file

@ -0,0 +1,49 @@
<template>
<div class="exports-modal__export">
<div class="exports-modal__info">
<div>
<div class="exports-modal__name">
{{ name }}
</div>
<div class="exports-modal__detail">
{{ $t('snapshotListItem.created') }} {{ timeAgo }}
</div>
</div>
</div>
<div class="exports-modal__actions">
<DownloadLink
:url="exportJob.url"
:filename="exportJob.exported_file_name"
:loading-class="'button--loading'"
>
{{ $t('exportWorkspaceModal.download') }}
</DownloadLink>
</div>
</div>
</template>
<script>
import timeAgo from '@baserow/modules/core/mixins/timeAgo'
import moment from '@baserow/modules/core/moment'
export default {
mixins: [timeAgo],
props: {
exportJob: {
type: Object,
required: true,
},
workspace: {
type: Object,
required: true,
},
},
computed: {
name() {
return `${this.workspace.name} - ${moment(
this.exportJob.created_on
).format('YYYY-MM-DD HH:mm:ss')}`
},
},
}
</script>

View file

@ -14,41 +14,47 @@
<Error :error="error"></Error>
<div class="export-workspace-modal">
<ExportWorkspaceForm ref="form" @submitted="submitted">
<template v-if="jobIsRunning || jobHasSucceeded" #settings>
<ProgressBar
:value="job.progress_percentage"
:status="jobHumanReadableState"
/>
<template v-if="jobIsRunning || jobHasSucceeded" #select-applications>
<div class="margin-right-2">
<ProgressBar
:value="job.progress_percentage"
:status="jobHumanReadableState"
/>
</div>
</template>
<template #default>
<Button
v-if="!loading && !finished"
v-if="!createFinished"
size="large"
:loading="loading"
:disabled="loading"
:loading="createLoading"
:disabled="createLoading || exportJobLoading"
>
{{ $t('exportWorkspaceModal.export') }}
</Button>
<Button
v-if="loading && !finished"
type="secondary"
tag="a"
size="large"
@click="reset()"
>
{{ $t('exportWorkspaceModal.cancel') }}</Button
>
<DownloadLink
v-if="!loading && finished"
class="button button--large button--full-width modal-progress__export-button"
:url="job.url"
:filename="job.exported_file_name"
:loading-class="'button--loading'"
>
{{ $t('exportTableLoadingBar.download') }}
</DownloadLink>
<Button v-else type="secondary" tag="a" size="large" @click="reset()">
{{ $t('exportWorkspaceModal.reset') }}
</Button>
</template>
</ExportWorkspaceForm>
<div class="snapshots-modal__list">
<div
v-if="exportJobLoading"
class="loading snapshots-modal__list--loading"
></div>
<div v-else-if="exportJobs.length > 0">
<ExportWorkspaceListItem
v-for="job in exportJobs"
ref="exportsList"
:key="job.id"
:export-job="job"
:workspace="workspace"
:last-updated="job.created_on"
></ExportWorkspaceListItem>
</div>
<div v-else>
{{ $t('exportWorkspaceModal.noExports') }}
</div>
</div>
</div>
<template #actions> </template>
</Modal>
@ -57,19 +63,21 @@
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
import SnapshotListItem from '@baserow/modules/core/components/snapshots/SnapshotListItem'
import WorkspaceService from '@baserow/modules/core/services/workspace'
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
import job from '@baserow/modules/core/mixins/job'
import ExportWorkspaceForm from '@baserow/modules/core/components/export/ExportWorkspaceForm'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { ExportApplicationsJobType } from '@baserow/modules/core/jobTypes'
import ExportWorkspaceListItem from '@baserow/modules/core/components/export/ExportWorkspaceListItem.vue'
const WORKSPACE_EXPORTS_LIMIT = 5
export default {
name: 'ExportWorkspaceModal',
components: {
ExportWorkspaceForm,
SnapshotListItem,
ExportWorkspaceListItem,
},
mixins: [modal, error, jobProgress],
mixins: [modal, error, job],
props: {
workspace: {
type: Object,
@ -78,9 +86,10 @@ export default {
},
data() {
return {
job: null,
loading: false,
finished: false,
createLoading: false,
createFinished: false,
exportJobLoading: false,
exportJobs: [],
}
},
computed: {
@ -92,54 +101,89 @@ export default {
.filter((component) => component !== null)
},
},
beforeDestroy() {
this.stopPollIfRunning()
},
methods: {
show(...args) {
modal.methods.show.bind(this)(...args)
this.reset()
this.loadExports()
modal.methods.show.bind(this)(...args)
},
async submitted(values) {
this.loading = true
this.createLoading = true
this.hideError()
try {
const { data } = await WorkspaceService(
const { data: job } = await WorkspaceService(
this.$client
).exportApplications(this.workspace.id, values)
this.startJobPoller(data)
this.job = job
await this.createAndMonitorJob(job)
} catch (error) {
this.loading = false
this.createLoading = false
this.handleError(error)
}
},
// eslint-disable-next-line require-await
async onJobDone() {
this.loading = false
this.finished = true
onJobDone() {
this.createLoading = false
this.createFinished = true
if (
this.job.type === ExportApplicationsJobType.getType() &&
this.job.workspace_id === this.workspace.id
) {
this.exportJobs.unshift(this.job)
this.exportJobs = this.exportJobs.splice(0, WORKSPACE_EXPORTS_LIMIT)
}
},
// eslint-disable-next-line require-await
async onJobFailed() {
this.loading = false
this.createLoading = false
this.showError(
this.$t('clientHandler.notCompletedTitle'),
this.job.human_readable_error
)
},
async loadExports() {
this.exportJobLoading = true
// eslint-disable-next-line require-await
async onJobPollingError(error) {
this.loading = false
notifyIf(error)
try {
const { data: exportJobs } = await WorkspaceService(
this.$client
).listExports(this.workspace.id)
this.exportJobs = exportJobs?.results || []
} catch (error) {
this.handleError(error)
} finally {
this.exportJobLoading = false
}
this.loadRunningJob()
},
loadRunningJob() {
const runningJob = this.$store.getters['job/getUnfinishedJobs'].find(
(job) => {
return (
job.type === ExportApplicationsJobType.getType() &&
job.workspace_id === this.workspace.id
)
}
)
if (runningJob) {
this.job = runningJob
this.createLoading = true
}
},
reset() {
this.stopPollIfRunning()
this.job = null
this.finished = false
this.loading = false
this.createFinished = false
this.createLoading = false
this.hideError()
},
getCustomHumanReadableJobState(jobState) {
if (jobState.startsWith('importing')) {
return this.$t('exportWorkspaceModal.importingState')
}
return ''
},
},
}
</script>

View file

@ -7,7 +7,7 @@
</div>
<div class="snapshots-modal__detail">
{{ snapshot.created_by ? `${snapshot.created_by.username} - ` : '' }}
{{ $t('snapshotListItem.created') }} {{ humanCreatedAt }}
{{ $t('snapshotListItem.created') }} {{ timeAgo }}
</div>
</div>
<ProgressBar
@ -44,28 +44,22 @@
<script>
import SnapshotsService from '@baserow/modules/core/services/snapshots'
import DeleteSnapshotModal from '@baserow/modules/core/components/snapshots/DeleteSnapshotModal'
import { getHumanPeriodAgoCount } from '@baserow/modules/core/utils/date'
import { notifyIf } from '@baserow/modules/core/utils/error'
import job from '@baserow/modules/core/mixins/job'
import timeAgo from '@baserow/modules/core/mixins/timeAgo'
import { RestoreSnapshotJobType } from '@baserow/modules/core/jobTypes'
export default {
components: {
DeleteSnapshotModal,
},
mixins: [job],
mixins: [job, timeAgo],
props: {
snapshot: {
type: Object,
required: true,
},
},
computed: {
humanCreatedAt() {
const { period, count } = getHumanPeriodAgoCount(this.snapshot.created_at)
return this.$tc(`datetime.${period}Ago`, count)
},
},
mounted() {
if (!this.job) {
this.restoreRunningState()

View file

@ -75,6 +75,7 @@
ref="snapshotsList"
:key="snapshot.id"
:snapshot="snapshot"
:last-updated="snapshot.created_at"
@snapshot-deleted="snapshotDeleted"
></SnapshotListItem>
</div>
@ -165,7 +166,6 @@ export default {
this.job.type === CreateSnapshotJobType.getType()
) {
this.snapshots.unshift(this.job.snapshot)
this.refreshSnapshots()
}
},
onJobFailed() {
@ -178,17 +178,6 @@ export default {
onJobCancelled() {
this.createLoading = false
},
/**
* This should be called if .snapshots list was changed without an API call issued.
*
* Each SnapshotListItem displays time elapsed from snapshot's creation. When an
* item is added to snapshots list, only that element will be freshly rendered.
* Other items won't recalculate time elapsed, so we need to do a force refresh
* from the parent component.
*/
refreshSnapshots() {
this.$forceUpdate()
},
async loadSnapshots() {
this.snapshotsLoading = true

View file

@ -254,3 +254,13 @@ export class RestoreSnapshotJobType extends JobType {
return 'restoreSnapshot'
}
}
export class ExportApplicationsJobType extends JobType {
static getType() {
return 'export_applications'
}
getName() {
return 'exportApplications'
}
}

View file

@ -132,7 +132,11 @@
"description": "Your data will be exported as a ZIP file, which can be imported into other Baserow instance.",
"exportSettings": "Export settings",
"export": "Export data",
"cancel": "Cancel"
"reset": "Start new",
"cancel": "Cancel",
"download": "Download",
"importingState": "Importing",
"noExports": "No exports for this workspace yet."
},
"dashboardWorkspace": {
"createApplication": "Create new"
@ -541,7 +545,9 @@
"hoursAgo": "0 hours ago | 1 hour ago | {n} hours ago",
"daysAgo": "0 days ago | 1 day ago | {n} days ago",
"monthsAgo": "0 months ago | 1 month ago | {n} months ago",
"yearsAgo": "0 years ago | 1 year ago | {n} years ago"
"yearsAgo": "0 years ago | 1 year ago | {n} years ago",
"lessThanMinuteAgo": "less than minute ago",
"justNow": "just now"
},
"crudTableSearch": {
"search": "Search"

View file

@ -0,0 +1,49 @@
import { getHumanPeriodAgoCount } from '@baserow/modules/core/utils/date'
export default {
props: {
lastUpdated: {
type: String,
required: true,
},
},
data() {
return {
timeAgo: '',
refreshPeriod: null,
timeoutHandler: null,
}
},
mounted() {
this.updateTimeAgo()
},
beforeDestroy() {
clearTimeout(this.timeoutHandler)
},
methods: {
updateTimeAgo() {
const { period, count } = getHumanPeriodAgoCount(this.lastUpdated)
if (period === 'seconds' && count <= 5) {
this.timeAgo = this.$t('datetime.justNow')
this.refreshPeriod = 5 * 1000
} else if (period === 'seconds') {
this.timeAgo = this.$t('datetime.lessThanMinuteAgo')
this.refreshPeriod = 60 * 1000
} else {
this.timeAgo = this.$tc(`datetime.${period}Ago`, count)
this.refreshPeriod = period === 'minutes' ? 60 * 1000 : 3600 * 1000
}
if (this.refreshPeriod) {
this.scheduleNextUpdate()
}
},
scheduleNextUpdate() {
clearTimeout(this.timeoutHandler)
this.timeoutHandler = setTimeout(() => {
this.updateTimeAgo()
}, this.refreshPeriod)
},
},
}

View file

@ -5,6 +5,7 @@ import { PasswordAuthProviderType } from '@baserow/modules/core/authProviderType
import {
CreateSnapshotJobType,
DuplicateApplicationJobType,
ExportApplicationsJobType,
InstallTemplateJobType,
RestoreSnapshotJobType,
} from '@baserow/modules/core/jobTypes'
@ -191,6 +192,7 @@ export default (context, inject) => {
registry.register('job', new InstallTemplateJobType(context))
registry.register('job', new CreateSnapshotJobType(context))
registry.register('job', new RestoreSnapshotJobType(context))
registry.register('job', new ExportApplicationsJobType(context))
registry.register(
'workspaceSettingsPage',

View file

@ -77,6 +77,9 @@ export default (client) => {
exportApplications(workspaceId, values) {
return client.post(`/workspaces/${workspaceId}/export/async/`, values)
},
listExports(workspaceId) {
return client.get(`/workspaces/${workspaceId}/export/`)
},
}
)
}