Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSES/CLA-signed-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ C/ My company has custom contribution contract with Lutra Consulting Ltd. or I a

* PeterPetrik, 31st March 2023
* tomasMizera, 31st March 2023
* MarcelGeo, 19th April 2023
* harminius, 18th Aril 2023
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
<dashboard-usage-info-row />
</template>
<template #content>
<dashboard-access-requests-row v-if="isGlobalWorkspaceAdmin" />
<dashboard-access-requests-row v-if="isGlobalWorkspaceAdmin">
<template v-slot:table="{ namespace }">
<project-access-request-table :namespace="namespace" />
</template>
</dashboard-access-requests-row>
<dashboard-projects-row>
<template #projects>
<projects-table-data-loader
Expand All @@ -34,7 +38,8 @@ import {
DashboardProjectsRow,
DashboardAccessRequestsRow,
ProjectsTableDataLoader,
DashboardUsageInfoRow
DashboardUsageInfoRow,
ProjectAccessRequestTable
} from '@mergin/lib'
import { computed, ref, onMounted } from '@vue/composition-api'
import { useActions, useGetters } from 'vuex-composition-helpers'
Expand All @@ -46,7 +51,8 @@ export default {
DashboardProjectsRow,
DashboardAccessRequestsRow,
ProjectsTableDataLoader,
DashboardUsageInfoRow
DashboardUsageInfoRow,
ProjectAccessRequestTable
},
setup() {
const initialOptions = ref({
Expand Down
7 changes: 7 additions & 0 deletions web-app/packages/lib/src/common/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,10 @@ function restartRequest(request) {
request.data = request.data ? JSON.parse(request.data) : request.data
return HTTP.request(request)
}

/**
* Axios default status codes overwrite for 207
*/
export function validateStatus(status: number): boolean {
return (status >= 200 && status < 207) || (status > 207 && status < 300)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
<v-card class="bubble mt-3">
<h3>Project access requests</h3>
<v-card-text>
<project-access-request-table :namespace="currentNamespace" />
<slot name="table" :namespace="currentNamespace"></slot>
</v-card-text>
</v-card>
</v-row>
Expand All @@ -25,11 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
import Vue from 'vue'
import { mapActions, mapState } from 'vuex'

import ProjectAccessRequestTable from '@/modules/project/components/ProjectAccessRequestTable.vue'

export default Vue.extend({
name: 'DashboardAccessRequestsRow',
components: { ProjectAccessRequestTable },
computed: {
...mapState('projectModule', [
'namespaceAccessRequests',
Expand Down
6 changes: 5 additions & 1 deletion web-app/packages/lib/src/modules/form/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ const FormStore: Module<FormState, RootState> = {
let errorMessage
if (typeof payload.error?.response?.data === 'object') {
// two types of error responses
if (typeof payload.error.response.data.status === 'number') {
// TODO: Get data from HTTP status code, handle formError not standard error with detail
if (
typeof payload.error.response.data.status === 'number' ||
payload.error.response.data.detail
) {
errorMessage = payload.error.response.data.detail
} else {
commit('setFormErrors', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export default Vue.extend({
'cancelProjectAccessRequest',
'acceptProjectAccessRequest'
]),
...mapActions('notificationModule', ['error']),

canAcceptAccessRequest(userId: number, expire: string) {
return (
Expand All @@ -213,19 +214,26 @@ export default Vue.extend({
)
},
async acceptRequest(request) {
const el = this.$refs['hidden-btn']
el.value = this.permissions[request.id]
el.dispatchEvent(new Event('click', {}))
const data = {
permissions: this.permissions[request.id]
try {
const el = this.$refs['hidden-btn']
el.value = this.permissions[request.id]
el.dispatchEvent(new Event('click', {}))
const data = {
permissions: this.permissions[request.id]
}
await this.acceptProjectAccessRequest({
data,
itemId: request.id,
refetchGlobalAccessRequests: false,
namespace: this.project.namespace,
projectName: this.project.name
})
} catch (err) {
const msg = err.response
? err.response.data?.detail
: 'Failed to accept access request'
this.error({ text: msg })
}
await this.acceptProjectAccessRequest({
data,
itemId: request.id,
refetchGlobalAccessRequests: false,
namespace: this.project.namespace,
projectName: this.project.name
})
},
expired(expire) {
return Date.parse(expire) < Date.now()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,210 +5,39 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
-->

<template>
<div>
<v-data-table
:headers="header"
:items="accessRequestsData"
ref="table"
no-data-text="No access requests"
color="primary"
:footer-props="{ 'items-per-page-options': [10, 25, 50] }"
:hide-default-footer="accessRequestsData.length <= 10"
:options="options"
>
<template #item.expire="{ value }">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<span v-on="on">{{ value | remainingtime }}</span>
</template>
<span>{{ value | datetime }}</span>
</v-tooltip>
</template>
<template #item.permission="{ item }">
<v-select
:items="['read', 'write', 'owner']"
v-model="permissions[item.id]"
return-object
:disabled="!showAccept"
>
</v-select>
</template>
<template #item.buttons="{ item }">
<div class="justify-center">
<div style="text-align: end">
<v-tooltip bottom v-if="showAccept">
<template v-slot:activator="{ on }">
<span v-on="on">
<v-chip
:disabled="expired(item.expire)"
@click="acceptRequest(item)"
elevation="0"
color="green"
class="white--text"
:value="permissions[item.id]"
>
accept
</v-chip>
</span>
</template>
<span>Accept request</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<span v-on="on">
<v-chip
@click="cancelRequest(item)"
elevation="0"
color="red"
class="white--text"
>
cancel
</v-chip>
</span>
</template>
<span>Cancel request</span>
</v-tooltip>
</div>
</div>
</template>
</v-data-table>
<button
ref="hidden-btn"
id="accept-request-access-btn"
style="visibility: hidden"
/>
</div>
<project-access-request-table-template
:namespace="namespace"
@accept-access-request-error="onAcceptAccessRequestError"
/>
</template>

<script lang="ts">
import { AxiosError } from 'axios'
import Vue from 'vue'
import { mapActions, mapState } from 'vuex'
import { mapActions } from 'vuex'
import ProjectAccessRequestTableTemplate from './ProjectAccessRequestTableTemplate.vue'

export default Vue.extend({
name: 'ProjectAccessRequestTable',
components: { ProjectAccessRequestTableTemplate },
props: {
namespace: {
type: String,
default: null
}
},
data() {
return {
options: {
'sort-by': 'name'
},
projectAccessRequests: [],
permissions: {}
}
},
computed: {
...mapState(['transfers']),
...mapState('projectModule', ['accessRequests', 'namespaceAccessRequests']),
accessRequestsData() {
return this.namespace == null
? this.accessRequests
: this.namespaceAccessRequests
},
showAccept() {
return this.namespace != null
},
header() {
return [
...(this.namespace == null
? []
: [{ text: 'Requester', value: 'requested_by', sortable: true }]),
{ text: 'Project name', value: 'project_name', sortable: true },
{ text: 'Expire in', value: 'expire', sortable: true },
{
text: 'Permissions',
value: 'permission',
width: 120,
sortable: false
},
{ text: '', value: 'buttons', width: 190, sortable: false }
]
}
},
async created() {
await this.reloadProjectAccessRequests({
refetchGlobalAccessRequests: true,
namespace: this.namespace
})
this.accessRequestsData.forEach((request) => {
this.permissions[request.id] = 'read'
})
},
methods: {
...mapActions('projectModule', [
'cancelProjectAccessRequest',
'acceptProjectAccessRequest',
'reloadProjectAccessRequests'
]),

async acceptRequest(request) {
const el = this.$refs['hidden-btn']
el.value = this.permissions[request.id]
el.dispatchEvent(new Event('click', {}))

const data = {
permissions: this.permissions[request.id]
}
await this.acceptProjectAccessRequest({
data,
itemId: request.id,
refetchGlobalAccessRequests: true,
namespace: this.namespace
})
},
changeSort(column) {
if (this.options.sortBy === column) {
this.options.descending = !this.options.descending
} else {
this.options.sortBy = column
this.options.descending = false
}
},
expired(expire) {
return Date.parse(expire) < Date.now()
},
async cancelRequest(request) {
await this.cancelProjectAccessRequest({
itemId: request.id,
refetchGlobalAccessRequests: true,
namespace: this.namespace
})
...mapActions('notificationModule', ['error']),

// TODO: Add more handlers from template, add more emits from template
async onAcceptAccessRequestError(err: AxiosError) {
const msg = err.response
? err.response.data?.detail
: 'Failed to accept access request'
this.error({ text: msg })
}
}
})
</script>

<style lang="scss" scoped>
.v-data-table {
td {
text-align: left;

&.flags {
.v-icon {
margin: 0 1px;
cursor: default;
}
}
}

a {
text-decoration: none;
}

.v-chip {
margin: 0;
margin-right: 0.5em;
height: 1.6em;

::v-deep .v-chip__content {
cursor: pointer;
padding: 0 0.5em;
font-size: 85%;
}
}
}
</style>
<style lang="scss" scoped></style>
Loading