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:
parent
6751a676ed
commit
685e8240cd
18 changed files with 539 additions and 100 deletions
backend
src/baserow
tests/baserow
api/import_export
contrib/database/export
web-frontend/modules/core
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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"):
|
||||
"""
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -254,3 +254,13 @@ export class RestoreSnapshotJobType extends JobType {
|
|||
return 'restoreSnapshot'
|
||||
}
|
||||
}
|
||||
|
||||
export class ExportApplicationsJobType extends JobType {
|
||||
static getType() {
|
||||
return 'export_applications'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'exportApplications'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
49
web-frontend/modules/core/mixins/timeAgo.js
Normal file
49
web-frontend/modules/core/mixins/timeAgo.js
Normal 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)
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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/`)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue