1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 14:25:37 +00:00

Resolve "Show database and tables duplication job progress in the left sidebar"

This commit is contained in:
Davide Silvestri 2022-09-15 16:57:29 +00:00
parent 5a60bb0d9a
commit 335a40255d
23 changed files with 688 additions and 194 deletions

View file

@ -26,12 +26,7 @@ from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.view_types import GridViewType
from baserow.core.registries import application_type_registry
from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import (
ChildProgressBuilder,
Progress,
find_unused_name,
split_ending_number,
)
from baserow.core.utils import ChildProgressBuilder, Progress, find_unused_name
from .constants import TABLE_CREATION
from .exceptions import (
@ -410,8 +405,7 @@ class TableHandler:
"""
existing_tables_names = list(database.table_set.values_list("name", flat=True))
name, _ = split_ending_number(proposed_name)
return find_unused_name([name], existing_tables_names, max_length=255)
return find_unused_name([proposed_name], existing_tables_names, max_length=255)
def _create_related_link_fields_in_existing_tables_to_import(
self, serialized_table: Dict[str, Any], id_mapping: Dict[str, Any]
@ -476,18 +470,20 @@ class TableHandler:
if not isinstance(table, Table):
raise ValueError("The table is not an instance of Table")
progress = ChildProgressBuilder.build(progress_builder, child_total=2)
start_progress, export_progress, import_progress = 10, 30, 60
progress = ChildProgressBuilder.build(progress_builder, child_total=100)
progress.increment(by=start_progress)
database = table.database
database.group.has_user(user, raise_error=True)
database_type = application_type_registry.get_by_model(database)
serialized_tables = database_type.export_tables_serialized([table])
progress.increment()
# Set a unique name for the table to import back as a new one.
exported_table = serialized_tables[0]
exported_table["name"] = self.find_unused_table_name(database, table.name)
exported_table["order"] = Table.get_last_order(database)
id_mapping: Dict[str, Any] = {"database_tables": {}}
@ -496,13 +492,17 @@ class TableHandler:
exported_table, id_mapping
)
)
progress.increment(by=export_progress)
imported_tables = database_type.import_tables_serialized(
database,
[exported_table],
id_mapping,
external_table_fields_to_import=link_fields_to_import_to_existing_tables,
progress_builder=progress.create_child_builder(
represents_progress=import_progress
),
)
progress.increment()
new_table_clone = imported_tables[0]

View file

@ -53,7 +53,9 @@ class DuplicateTableJobType(JobType):
new_table_clone = action_type_registry.get_by_type(DuplicateTableActionType).do(
job.user,
job.original_table,
progress.create_child_builder(represents_progress=progress.total),
progress_builder=progress.create_child_builder(
represents_progress=progress.total
),
)
# update the job with the new duplicated table

View file

@ -847,25 +847,32 @@ class CoreHandler:
group = application.group
group.has_user(user, raise_error=True)
progress = ChildProgressBuilder.build(progress_builder, child_total=2)
start_progress, export_progress, import_progress = 10, 30, 60
progress = ChildProgressBuilder.build(progress_builder, child_total=100)
progress.increment(by=start_progress)
# export the application
specific_application = application.specific
application_type = application_type_registry.get_by_model(specific_application)
serialized = application_type.export_serialized(specific_application)
progress.increment()
progress.increment(by=export_progress)
# Set a new unique name for the new application
serialized["name"] = self.find_unused_application_name(
group.id, serialized["name"]
)
serialized["order"] = application_type.model_class.get_last_order(group)
# import it back as a new application
id_mapping: Dict[str, Any] = {}
new_application_clone = application_type.import_serialized(
group, serialized, id_mapping
group,
serialized,
id_mapping,
progress_builder=progress.create_child_builder(
represents_progress=import_progress
),
)
progress.increment()
# broadcast the application_created signal
application_created.send(

View file

@ -45,6 +45,7 @@ For example:
* Add API token authentication support to multipart and via-URL file uploads. [#255](https://gitlab.com/bramw/baserow/-/issues/255)
* Add a rich preview while importing data to an existing table. [#1120](https://gitlab.com/bramw/baserow/-/issues/1120)
* Add env vars for controlling which URLs and IPs webhooks are allowed to use. [#931](https://gitlab.com/bramw/baserow/-/issues/931)
* Show database and table duplication progress in the left sidebar. [#1059](https://gitlab.com/bramw/baserow/-/issues/1059)
### Bug Fixes
* Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113)

View file

@ -10,6 +10,10 @@
}
}
.tree:not(:last-child) {
margin-bottom: 3px;
}
.tree__item {
@extend %first-last-no-margin;
@ -40,6 +44,11 @@
padding: 0 6px;
border-radius: 3px;
&.tree__action--disabled {
opacity: 0.6;
cursor: not-allowed;
}
&:not(.tree__action--disabled):hover {
background-color: $color-neutral-100;
}
@ -79,6 +88,13 @@
}
}
.tree__progress_percentage {
display: inline-block;
position: absolute;
right: 35px;
color: $color-primary-500;
}
.tree__icon {
@extend %tree__size;
@ -135,6 +151,14 @@
}
}
.tree__subs:not(:last-of-type) {
padding: 0;
& .tree__sub:last-child::before {
height: 28px;
}
}
.tree__sub-link {
@extend %tree_sub-size;
@extend %ellipsis;
@ -154,6 +178,23 @@
}
}
.tree__sub-link--loading {
@include loading(14px);
@include absolute(9px, 9px, auto, auto);
}
.tree__sub-link--disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
color: $color-primary-900;
opacity: 0.6;
text-decoration: none;
cursor: not-allowed;
}
}
.tree__sub-add {
display: inline-block;
margin: 0 0 10px 10px;

View file

@ -179,6 +179,15 @@
:group="selectedGroup"
></component>
</ul>
<ul v-if="pendingJobs.length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs"
:key="job.id"
:job="job"
>
</component>
</ul>
<li class="sidebar__new-wrapper">
<a
ref="createApplicationContextLink"
@ -329,6 +338,13 @@ export default {
.map((plugin) => plugin.getSidebarMainMenuComponent())
.filter((component) => component !== null)
},
pendingJobs() {
return this.$store.getters['job/getAll'].filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfGroup(job, this.selectedGroup)
)
},
/**
* Indicates whether the current user is visiting an admin page.
*/
@ -358,6 +374,9 @@ export default {
.get('application', application.type)
.getSidebarComponent()
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
logoff() {
this.$store.dispatch('auth/logoff')
this.$nuxt.$router.push({ name: 'login' })

View file

@ -38,27 +38,18 @@
<a @click="enableRename()">
<i class="context__menu-icon fas fa-fw fa-pen"></i>
{{
$t('sidebarApplication.renameApplication', {
$t('sidebarApplication.rename', {
type: application._.type.name.toLowerCase(),
})
}}
</a>
</li>
<li>
<a
:class="{
'context__menu-item--loading': duplicateLoading,
disabled: duplicateLoading || deleteLoading,
}"
@click="duplicateApplication()"
>
<i class="context__menu-icon fas fa-fw fa-copy"></i>
{{
$t('sidebarApplication.duplicateApplication', {
type: application._.type.name.toLowerCase(),
})
}}
</a>
<SidebarDuplicateApplicationContextItem
:application="application"
:disabled="deleting"
@click="$refs.context.hide()"
></SidebarDuplicateApplicationContextItem>
</li>
<li>
<a @click="openSnapshots">
@ -78,12 +69,12 @@
</li>
<li>
<a
:class="{ 'context__menu-item--loading': deleteLoading }"
:class="{ 'context__menu-item--loading': deleting }"
@click="deleteApplication()"
>
<i class="context__menu-icon fas fa-fw fa-trash"></i>
{{
$t('sidebarApplication.deleteApplication', {
$t('sidebarApplication.delete', {
type: application._.type.name.toLowerCase(),
})
}}
@ -103,17 +94,18 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import ApplicationService from '@baserow/modules/core/services/application'
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
import SidebarDuplicateApplicationContextItem from '@baserow/modules/core/components/sidebar/SidebarDuplicateApplicationContextItem.vue'
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
import SnapshotsModal from '@baserow/modules/core/components/snapshots/SnapshotsModal'
export default {
name: 'SidebarApplication',
components: { TrashModal, SnapshotsModal },
mixins: [jobProgress],
components: {
TrashModal,
SidebarDuplicateApplicationContextItem,
SnapshotsModal,
},
props: {
application: {
type: Object,
@ -126,18 +118,9 @@ export default {
},
data() {
return {
deleteLoading: false,
duplicateLoading: false,
deleting: false,
}
},
computed: {
...mapGetters({
selectedTable: 'table/getSelected',
}),
},
beforeDestroy() {
this.stopPollIfRunning()
},
methods: {
setLoading(application, value) {
this.$store.dispatch('application/setItemLoading', {
@ -166,92 +149,12 @@ export default {
this.setLoading(application, false)
},
showError(title, message) {
this.$store.dispatch(
'notification/error',
{ title, message },
{ root: true }
)
},
// eslint-disable-next-line require-await
async onJobFailed() {
this.duplicateLoading = false
this.$refs.context.hide()
this.showError(
this.$t('clientHandler.notCompletedTitle'),
this.$t('clientHandler.notCompletedDescription')
)
},
// eslint-disable-next-line require-await
async onJobPollingError(error) {
this.duplicateLoading = false
this.$refs.context.hide()
notifyIf(error, 'application')
},
async onJobDone() {
const newApplicationId = this.job.duplicated_application.id
let newApplication
try {
newApplication = await this.$store.dispatch('application/fetch', {
applicationId: newApplicationId,
})
} catch (error) {
notifyIf(error, 'application')
} finally {
this.duplicateLoading = false
this.$refs.context.hide()
}
// find the matching table in the duplicated application if any
// otherwise just select the first table and show it
if (newApplication) {
if (newApplication.tables.length) {
let selectTable = newApplication.tables[0]
const originalSelectedTable = this.selectedTable
if (originalSelectedTable) {
for (const table of newApplication.tables) {
if (table.name === originalSelectedTable.name) {
selectTable = table
break
}
}
}
this.$nuxt.$router.push({
name: 'database-table',
params: {
databaseId: newApplication.id,
tableId: selectTable.id,
},
})
} else {
this.$emit('selected', newApplication)
}
}
},
async duplicateApplication() {
if (this.duplicateLoading || this.deleteLoading) {
return
}
const application = this.application
this.duplicateLoading = true
try {
const { data: job } = await ApplicationService(
this.$client
).asyncDuplicate(application.id)
this.startJobPoller(job)
} catch (error) {
this.duplicateLoading = false
notifyIf(error, 'application')
}
},
async deleteApplication() {
if (this.deleteLoading) {
if (this.deleting) {
return
}
this.deleteLoading = true
this.deleting = true
try {
await this.$store.dispatch('application/delete', this.application)
@ -263,7 +166,7 @@ export default {
notifyIf(error, 'application')
}
this.deleteLoading = false
this.deleting = false
},
showApplicationTrashModal() {
this.$refs.context.hide()

View file

@ -0,0 +1,36 @@
<template>
<li class="tree__item tree__item--loading">
<div class="tree__action tree__action--disabled">
<a class="tree__link">
<i
class="tree__icon tree__icon--type fas"
:class="'fa-' + jobIconClass"
></i>
{{ jobSidebarText }}
<div class="tree__progress_percentage">
{{ job.progress_percentage }} %
</div>
</a>
</div>
</li>
</template>
<script>
export default {
name: 'SidebarApplicationPendingJob',
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobSidebarText() {
return this.$registry.get('job', this.job.type).getSidebarText(this.job)
},
jobIconClass() {
return this.$registry.get('job', this.job.type).getIconClass()
},
},
}
</script>

View file

@ -0,0 +1,98 @@
<template>
<a
:class="{
'context__menu-item--loading': duplicating,
disabled: disabled || duplicating,
}"
@click="duplicateApplication()"
>
<i class="context__menu-icon fas fa-fw fa-copy"></i>
{{
$t('sidebarApplication.duplicate', {
type: application._.type.name.toLowerCase(),
})
}}
</a>
</template>
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import ApplicationService from '@baserow/modules/core/services/application'
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
export default {
name: 'SidebarDuplicateApplicationContextItem',
mixins: [jobProgress],
props: {
application: {
type: Object,
required: true,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
duplicating: false,
}
},
methods: {
showError(title, message) {
this.$store.dispatch(
'notification/error',
{ title, message },
{ root: true }
)
},
// eslint-disable-next-line require-await
async onJobFailed() {
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
this.duplicating = false
},
async onJobPollingError(error) {
await this.$store.dispatch('job/forceDelete', this.job)
this.duplicating = false
notifyIf(error, 'application')
},
async onJobUpdated() {
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
},
// eslint-disable-next-line require-await
async onJobDone() {
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
this.duplicating = false
},
async duplicateApplication() {
if (this.duplicating || this.disabled) {
return
}
this.duplicating = true
try {
const { data: job } = await ApplicationService(
this.$client
).asyncDuplicate(this.application.id)
this.startJobPoller(job)
this.$store.dispatch('job/forceCreate', job)
} catch (error) {
this.duplicating = false
notifyIf(error, 'application')
}
this.$emit('click')
},
},
}
</script>

View file

@ -0,0 +1,159 @@
import { notifyIf } from '@baserow/modules/core/utils/error'
import { Registerable } from '@baserow/modules/core/registry'
import SidebarApplicationPendingJob from '@baserow/modules/core/components/sidebar/SidebarApplicationPendingJob'
/**
* The job type base class that can be extended when creating a plugin
* for the frontend.
*/
export class JobType extends Registerable {
/**
* The font awesome 5 icon name that is used as convenience for the user to
* recognize certain job types. If you for example want the database
* icon, you must return 'database' here. This will result in the classname
* 'fas fa-database'.
*/
getIconClass() {
return null
}
/**
* A human readable name of the job type.
*/
getName() {
return null
}
/**
* Returns the text to show in the sidebar when the job is running.
*/
getSidebarText(job) {
return ''
}
/**
* The sidebar component will be rendered in the sidebar if the application is
* in the selected group. All the applications of a group are listed in the
* sidebar and this component should give the user the possibility to select
* that application.
*/
getSidebarComponent() {
return null
}
constructor(...args) {
super(...args)
this.type = this.getType()
if (this.type === null) {
throw new Error('The type name of an application type must be set.')
}
if (this.name === null) {
throw new Error('The name of an application type must be set.')
}
}
/**
* Every time a fresh job object is fetched from the backend, it will
* be populated, this is the moment to update some values. Because each
* jobType can have unique properties, they might need to be populated.
* This method can be overwritten in order the populate the correct values.
*/
populate(job) {
return job
}
/**
* Returns true if the specific job is part of the group
*/
isJobPartOfGroup(job, group) {
return false
}
/**
* Returns true if the specific job is part of the application
*/
isJobPartOfApplication(job, application) {
return false
}
/**
* When an application is deleted it could be that an action should be taken,
* like redirect the user to another page. This method is called when application
* of this type is deleted.
*/
beforeDelete(job) {}
/**
* Before the application values are updated, they can be modified here. This
* might be needed because providing certain values could break the update.
*/
async beforeUpdate(job, data) {}
async afterUpdate(job, data) {
if (job.state === 'finished') {
await this.onJobDone(job, data)
} else if (job.state === 'failed') {
await this.onJobFailed(job, data)
}
}
async onJobDone(job) {}
async onJobFailed(job) {}
}
export class DuplicateApplicationJobType extends JobType {
static getType() {
return 'duplicate_application'
}
getIconClass() {
// TODO: This should be moved to a registry and in the database module.
return 'database'
}
getName() {
return 'duplicateApplication'
}
getSidebarText(job) {
const { i18n } = this.app
return i18n.t('duplicateApplicationJobType.duplicating') + '... '
}
getSidebarComponent() {
return SidebarApplicationPendingJob
}
isJobPartOfGroup(job, group) {
return job.original_application.group.id === group.id
}
async onJobDone(job) {
const { i18n, store } = this.app
const application = job.duplicated_application
try {
await store.dispatch('application/fetch', application.id)
store.dispatch('notification/info', {
title: i18n.t('duplicateApplicationJobType.duplicatedTitle'),
message: application.name,
})
await store.dispatch('job/forceDelete', job)
} catch (error) {
notifyIf(error, 'application')
}
}
async onJobFailed(job) {
const { i18n, store } = this.app
await store.dispatch(
'notification/error',
{
title: i18n.t('clientHandler.notCompletedTitle'),
message: i18n.t('clientHandler.notCompletedDescription'),
},
{ root: true }
)
await this.app.store.dispatch('job/forceDelete', job)
}
}

View file

@ -7,12 +7,16 @@
"label": "Copied!"
},
"sidebarApplication": {
"renameApplication": "Rename {type}",
"duplicateApplication": "Duplicate {type}",
"rename": "Rename",
"duplicate": "Duplicate",
"viewTrash": "View trash",
"deleteApplication": "Delete {type}",
"delete": "Delete",
"snapshots": "Snapshots"
},
"duplicateApplicationJobType": {
"duplicating": "Duplicating",
"duplicatedTitle": "Application duplicated"
},
"sidebar": {
"createGroup": "Create group",
"inviteOthers": "Invite others",
@ -397,4 +401,4 @@
"monthsAgo": "0 months ago | 1 month ago | {n} months ago",
"yearsAgo": "0 years ago | 1 year ago | {n} years ago"
}
}
}

View file

@ -4,35 +4,34 @@ import JobService from '@baserow/modules/core/services/job'
* To use this mixin you need to create the following methods on your component:
* - `getCustomHumanReadableJobState(state)` returns the human readable message your
* custom state values.
* - onJobDone() (optional) is called when the job is finished
* - onJobFailed() (optional) is called if the job failed
* - onJobPollingError(error) (optional) is there is an exception during the job polling
* - onJobUpdated() (optional) is called during the polling for not finished jobs.
* - onJobDone() (optional) is called if the job successfully finishes.
* - onJobFailed() (optional) is called if the job fails.
* - onJobPollingError(error) (optional) is called if the polling fails.
*
*/
export default {
data() {
return {
job: null,
pollInterval: null,
nextPollTimeout: null,
pollTimeoutId: null,
}
},
computed: {
jobIsRunning() {
return (
this.job !== null && !['failed', 'finished'].includes(this.job.state)
)
},
jobIsFinished() {
return (
this.job !== null && ['failed', 'finished'].includes(this.job.state)
)
},
jobHasSucceeded() {
return this.job?.state === 'finished'
},
jobHasFailed() {
return this.job?.state === 'failed'
},
jobIsFinished() {
return this.jobHasSucceeded || this.jobHasFailed
},
jobIsRunning() {
return this.job !== null && !this.jobIsFinished
},
jobHumanReadableState() {
if (this.job === null) {
return ''
@ -57,35 +56,46 @@ export default {
*/
startJobPoller(job) {
this.job = job
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
this.nextPollTimeout = 200
this.pollTimeoutId = setTimeout(
this.getLatestJobInfo,
this.nextPollTimeout
)
},
async getLatestJobInfo() {
try {
const { data } = await JobService(this.$client).get(this.job.id)
this.job = data
if (this.jobHasFailed) {
if (this.onJobFailed) {
const { data: job } = await JobService(this.$client).get(this.job.id)
this.job = job
if (job.state === 'failed') {
if (typeof this.onJobFailed === 'function') {
await this.onJobFailed()
}
} else if (!this.jobIsRunning) {
if (this.onJobDone) {
} else if (job.state === 'finished') {
if (typeof this.onJobDone === 'function') {
await this.onJobDone()
}
} else {
// job unfinished, set the next polling timeout
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
// job unfinished, keep polling
if (typeof this.onJobUpdated === 'function') {
await this.onJobUpdated()
}
this.nextPollTimeout = Math.min(this.nextPollTimeout * 1.5, 2500)
this.pollTimeoutId = setTimeout(
this.getLatestJobInfo,
this.nextPollTimeout
)
}
} catch (error) {
if (this.onJobPollingError) {
if (typeof this.onJobPollingError === 'function') {
this.onJobPollingError(error)
}
this.job = null
}
},
stopPollIfRunning() {
if (this.pollInterval) {
clearTimeout(this.pollInterval)
this.pollInterval = null
if (this.pollTimeoutId) {
clearTimeout(this.pollTimeoutId)
this.pollTimeoutId = null
}
},
},

View file

@ -1,6 +1,7 @@
import Vue from 'vue'
import { Registry } from '@baserow/modules/core/registry'
import { DuplicateApplicationJobType } from '@baserow/modules/core/jobTypes'
import {
AccountSettingsType,
@ -17,6 +18,7 @@ import settingsStore from '@baserow/modules/core/store/settings'
import applicationStore from '@baserow/modules/core/store/application'
import authStore from '@baserow/modules/core/store/auth'
import groupStore from '@baserow/modules/core/store/group'
import jobStore from '@baserow/modules/core/store/job'
import notificationStore from '@baserow/modules/core/store/notification'
import sidebarStore from '@baserow/modules/core/store/sidebar'
import undoRedoStore from '@baserow/modules/core/store/undoRedo'
@ -46,6 +48,7 @@ export default (context, inject) => {
const registry = new Registry()
registry.registerNamespace('plugin')
registry.registerNamespace('application')
registry.registerNamespace('job')
registry.registerNamespace('view')
registry.registerNamespace('field')
registry.registerNamespace('settings')
@ -64,8 +67,11 @@ export default (context, inject) => {
store.registerModule('settings', settingsStore)
store.registerModule('application', applicationStore)
store.registerModule('auth', authStore)
store.registerModule('job', jobStore)
store.registerModule('group', groupStore)
store.registerModule('notification', notificationStore)
store.registerModule('sidebar', sidebarStore)
store.registerModule('undoRedo', undoRedoStore)
registry.register('job', new DuplicateApplicationJobType(context))
}

View file

@ -42,19 +42,23 @@ export const mutations = {
},
UPDATE_ITEM(state, { id, values }) {
const index = state.items.findIndex((item) => item.id === id)
Object.assign(state.items[index], state.items[index], values)
if (index !== -1) {
Object.assign(state.items[index], state.items[index], values)
}
},
ORDER_ITEMS(state, { group, order }) {
state.items
.filter((item) => item.group.id === group.id)
.forEach((item) => {
const index = order.findIndex((value) => value === item.id)
item.order = index === -1 ? 0 : index + 1
item.order = index === -1 ? undefined : index + 1
})
},
DELETE_ITEM(state, id) {
const index = state.items.findIndex((item) => item.id === id)
state.items.splice(index, 1)
if (index !== -1) {
state.items.splice(index, 1)
}
},
DELETE_ITEMS_FOR_GROUP(state, groupId) {
state.items = state.items.filter((app) => app.group.id !== groupId)
@ -111,9 +115,8 @@ export const actions = {
/**
* Fetches one application for the authenticated user.
*/
async fetch({ commit, dispatch }, { applicationId }) {
async fetch({ commit, dispatch }, applicationId) {
commit('SET_LOADING', true)
try {
const { data } = await ApplicationService(this.$client).get(applicationId)
dispatch('forceCreate', data)
@ -246,8 +249,9 @@ export const actions = {
/**
* Forcefully delete an item in the store without making a call to the server.
*/
forceDelete({ commit }, application) {
forceDelete({ commit, dispatch }, application) {
const type = this.$registry.get('application', application.type)
dispatch('job/deleteForApplication', application, { root: true })
type.delete(application, this)
commit('DELETE_ITEM', application.id)
},

View file

@ -226,6 +226,7 @@ export const actions = {
* example a Table is open that has been deleted because the group has been deleted.
*/
forceDelete({ commit, dispatch, rootGetters }, group) {
dispatch('job/deleteForGroup', group, { root: true })
const applications = rootGetters['application/getAllOfGroup'](group)
applications.forEach((application) => {
return dispatch('application/forceDelete', application, { root: true })

View file

@ -0,0 +1,90 @@
export function populateJob(job, registry) {
const type = registry.get('job', job.type)
return type.populate(job)
}
export const state = () => ({
items: [],
})
export const mutations = {
ADD_ITEM(state, item) {
state.items.push(item)
},
UPDATE_ITEM(state, { id, values }) {
const index = state.items.findIndex((item) => item.id === id)
if (index !== -1) {
Object.assign(state.items[index], state.items[index], values)
}
},
DELETE_ITEM(state, id) {
const index = state.items.findIndex((item) => item.id === id)
if (index !== -1) {
state.items.splice(index, 1)
}
},
}
export const actions = {
/**
* Forcefully create an item in the store without making a call to the server.
*/
forceCreate({ commit }, job) {
populateJob(job, this.$registry)
commit('ADD_ITEM', job)
},
/**
* Forcefully update an item in the store without making a call to the server.
*/
async forceUpdate({ commit }, { job, data }) {
const type = this.$registry.get('job', job.type)
await type.beforeUpdate(job, data)
commit('UPDATE_ITEM', { id: job.id, values: data })
await type.afterUpdate(job, data)
},
/**
* Forcefully delete an item in the store without making a call to the server.
*/
forceDelete({ commit }, job) {
const type = this.$registry.get('job', job.type)
type.beforeDelete(job, this)
commit('DELETE_ITEM', job.id)
},
/**
* Deletes all the jobs belonging to the given group.
*/
deleteForGroup({ commit, state }, group) {
const jobs = state.items.filter((job) =>
this.$registry.get('job', job.type).isJobPartOfGroup(job, group)
)
jobs.forEach((job) => commit('DELETE_ITEM', job.id))
},
/**
* Deletes all the jobs belonging to the given application.
*/
deleteForApplication({ commit, state }, application) {
const jobs = state.items.filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfApplication(job, application)
)
jobs.forEach((job) => commit('DELETE_ITEM', job.id))
},
}
export const getters = {
get: (state) => (id) => {
return state.items.find((item) => item.id === id)
},
getAll: (state) => {
return state.items
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}

View file

@ -35,6 +35,14 @@
:table="table"
></SidebarItem>
</ul>
<ul v-if="pendingJobs.length" class="tree__subs">
<SidebarItemPendingJob
v-for="job in pendingJobs"
:key="job.id"
:job="job"
>
</SidebarItemPendingJob>
</ul>
<a class="tree__sub-add" @click="$refs.importFileModal.show()">
<i class="fas fa-plus"></i>
{{ $t('sidebar.createTable') }}
@ -47,6 +55,7 @@
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import SidebarItem from '@baserow/modules/database/components/sidebar/SidebarItem'
import SidebarItemPendingJob from '@baserow/modules/database/components/sidebar/SidebarItemPendingJob'
import ImportFileModal from '@baserow/modules/database/components/table/ImportFileModal'
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
@ -55,6 +64,7 @@ export default {
components: {
SidebarApplication,
SidebarItem,
SidebarItemPendingJob,
ImportFileModal,
},
props: {
@ -73,6 +83,13 @@ export default {
.map((table) => table)
.sort((a, b) => a.order - b.order)
},
pendingJobs() {
return this.$store.getters['job/getAll'].filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfApplication(job, this.application)
)
},
},
methods: {
async selected(application) {

View file

@ -47,10 +47,7 @@
:database="database"
:table="table"
:disabled="deleteLoading"
@table-duplicated="
$refs.context.hide()
selectTable(database, $event.table)
"
@click="$refs.context.hide()"
></SidebarDuplicateTableContextItem>
</li>
<li>

View file

@ -0,0 +1,28 @@
<template>
<li class="tree__sub">
<a class="tree__sub-link tree__sub-link--disabled">
{{ jobSidebarText }}
<div class="tree__progress_percentage">
{{ job.progress_percentage }} %
</div>
</a>
<div class="tree__sub-link--loading"></div>
</li>
</template>
<script>
export default {
name: 'SidebarItemPendingJob',
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobSidebarText() {
return this.$registry.get('job', this.job.type).getSidebarText(this.job)
},
},
}
</script>

View file

@ -1,8 +1,8 @@
<template>
<a
:class="{
'context__menu-item--loading': loading,
disabled: disabled || loading,
'context__menu-item--loading': duplicating,
disabled: disabled || duplicating,
}"
@click="duplicateTable()"
>
@ -36,7 +36,7 @@ export default {
},
data() {
return {
loading: false,
duplicating: false,
}
},
methods: {
@ -47,44 +47,48 @@ export default {
{ root: true }
)
},
// eslint-disable-next-line require-await
async onJobFailed() {
this.loading = false
this.showError(
this.$t('clientHandler.notCompletedTitle'),
this.$t('clientHandler.notCompletedDescription')
)
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
this.duplicating = false
},
// eslint-disable-next-line require-await
async onJobPollingError(error) {
this.loading = false
await this.$store.dispatch('job/forceDelete', this.job)
this.duplicating = false
notifyIf(error, 'table')
},
async onJobDone() {
const database = this.database
const table = this.job.duplicated_table
await this.$store.dispatch('table/forceCreate', {
database,
data: table,
async onJobUpdated() {
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
this.loading = false
this.$emit('table-duplicated', { table })
},
async onJobDone() {
await this.$store.dispatch('job/forceUpdate', {
job: this.job,
data: this.job,
})
this.duplicating = false
},
async duplicateTable() {
if (this.loading || this.disabled) {
if (this.duplicating || this.disabled) {
return
}
this.loading = true
this.duplicating = true
try {
const { data: job } = await TableService(this.$client).asyncDuplicate(
this.table.id
)
this.startJobPoller(job)
this.$store.dispatch('job/forceCreate', job)
} catch (error) {
this.loading = false
this.duplicating = false
notifyIf(error, 'table')
}
this.$emit('click')
},
},
}

View file

@ -0,0 +1,61 @@
import { JobType } from '@baserow/modules/core/jobTypes'
import SidebarItemPendingJob from '@baserow/modules/database/components/sidebar/SidebarItemPendingJob.vue'
export class DuplicateTableJobType extends JobType {
static getType() {
return 'duplicate_table'
}
getName() {
return 'duplicateTable'
}
getSidebarText(job) {
const { i18n } = this.app
return i18n.t('duplicateTableJobType.duplicating') + '...'
}
getSidebarComponent() {
return SidebarItemPendingJob
}
isJobPartOfApplication(job, application) {
return job.original_table.database_id === application.id
}
async onJobFailed(job) {
const { i18n, store } = this.app
store.dispatch(
'notification/error',
{
title: i18n.t('clientHandler.notCompletedTitle'),
message: i18n.t('clientHandler.notCompletedDescription'),
},
{ root: true }
)
await store.dispatch('job/forceDelete', job)
}
async onJobDone(job) {
const { i18n, store } = this.app
const duplicatedTable = job.duplicated_table
const database = store.getters['application/get'](
duplicatedTable.database_id
)
await store.dispatch('table/forceCreate', {
database,
data: duplicatedTable,
})
store.dispatch('notification/info', {
title: i18n.t('duplicateTableJobType.duplicatedTitle'),
message: duplicatedTable.name,
})
store.dispatch('job/forceDelete', job)
}
}

View file

@ -71,6 +71,10 @@
"sidebarItem": {
"exportTable": "Export table"
},
"duplicateTableJobType": {
"duplicating": "Duplicating",
"duplicatedTitle": "Table duplicated"
},
"apiToken": {
"create": "create",
"read": "read",

View file

@ -1,4 +1,5 @@
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
import { DuplicateTableJobType } from '@baserow/modules/database/jobTypes'
import {
GridViewType,
GalleryViewType,
@ -225,6 +226,7 @@ export default (context) => {
app.$registry.register('plugin', new DatabasePlugin(context))
app.$registry.register('application', new DatabaseApplicationType(context))
app.$registry.register('job', new DuplicateTableJobType(context))
app.$registry.register('view', new GridViewType(context))
app.$registry.register('view', new GalleryViewType(context))
app.$registry.register('view', new FormViewType(context))