feat(frontend): minor cleanup & some tracking improvements
This commit is contained in:
Generated
+38363
-17127
File diff suppressed because it is too large
Load Diff
@@ -137,9 +137,10 @@ export default {
|
||||
mounted() {
|
||||
this.setNavResizeEvents()
|
||||
|
||||
let distinct_id = localStorage.getItem('distinct_id')
|
||||
if (distinct_id !== null) {
|
||||
this.$mixpanel.identify(distinct_id)
|
||||
let mixpanelId = this.$mixpanelId()
|
||||
if (mixpanelId !== null) {
|
||||
this.$mixpanel.identify(mixpanelId)
|
||||
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
this.$mixpanel.track('Visit Web App')
|
||||
|
||||
@@ -49,6 +49,7 @@ export default {
|
||||
if (!dialog.result) return
|
||||
|
||||
this.$mixpanel.track('User Action', { type: 'action', name: 'delete', hostApp: 'web' })
|
||||
this.$mixpanel.people.set('Account deleted', true)
|
||||
this.$matomo && this.$matomo.trackPageView('user/delete')
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
no-gutters
|
||||
:class="`my-1 py-1 property-row rounded-lg ${$vuetify.theme.dark ? 'black-bg' : 'white-bg'} ${
|
||||
prop.type === 'object' || prop.type === 'array' ? (expanded ? 'border-blue' : 'border') : ''
|
||||
} ${prop.type === 'object' || prop.type === 'array' ? 'hover-cursor property-row-hover' : 'normal-cursor'}`"
|
||||
} ${
|
||||
prop.type === 'object' || prop.type === 'array'
|
||||
? 'hover-cursor property-row-hover'
|
||||
: 'normal-cursor'
|
||||
}`"
|
||||
@click.stop="prop.type === 'object' || prop.type === 'array' ? (expanded = !expanded) : null"
|
||||
>
|
||||
<v-col cols="1" class="text-center">
|
||||
|
||||
@@ -201,7 +201,11 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$mixpanel.track('Share Stream', { type: 'action', hostApp: 'web' })
|
||||
this.$mixpanel.track('Share Stream', {
|
||||
type: 'action',
|
||||
location: this.$route.name,
|
||||
hostApp: 'web'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(e) {
|
||||
|
||||
@@ -442,7 +442,12 @@ export default {
|
||||
data: resId.length === 10 ? await this.loadCommit(resId) : await this.loadObject(resId)
|
||||
}
|
||||
this.resources.push(resource)
|
||||
|
||||
this.$mixpanel.track('Viewer Action', {
|
||||
type: 'action',
|
||||
name: 'add',
|
||||
resourceType: resource.type,
|
||||
hostApp: 'web'
|
||||
})
|
||||
// TODO add to url
|
||||
let fullQuery = { ...this.$route.query }
|
||||
delete fullQuery.overlay
|
||||
@@ -479,6 +484,13 @@ export default {
|
||||
: resource.data.object.id
|
||||
}`
|
||||
|
||||
this.$mixpanel.track('Viewer Action', {
|
||||
type: 'action',
|
||||
name: 'remove',
|
||||
resourceType: resource.type,
|
||||
hostApp: 'web'
|
||||
})
|
||||
|
||||
await window.__viewer.unloadObject(url)
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ import gql from 'graphql-tag'
|
||||
export default {
|
||||
name: 'Details',
|
||||
components: {
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
|
||||
NoDataPlaceholder: () => import('@/cleanup/components/common/NoDataPlaceholder'),
|
||||
ListItemCommit: () => import('@/cleanup/components/stream/ListItemCommit'),
|
||||
PreviewImage: () => import('@/components/PreviewImage'),
|
||||
PreviewImage: () => import('@/cleanup/components/common/PreviewImage'),
|
||||
StreamActivity: () => import('@/views/stream/Activity')
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<v-card rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Applications</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Register and manage third-party Speckle Apps that, once authorised by a user on this server,
|
||||
can act on their behalf.
|
||||
</v-card-text>
|
||||
<v-card-text v-if="$apollo.loading">Loading...</v-card-text>
|
||||
<v-card-text v-if="apps && apps.length !== 0">
|
||||
<v-list two-line class="transparent">
|
||||
<list-item-user-app
|
||||
v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
@app-edited="refreshList"
|
||||
@deleted="refreshList"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-text v-else>You have no apps.</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn class="mb-5" @click="appDialog = true">new app</v-btn>
|
||||
<v-dialog v-model="appDialog" width="500">
|
||||
<app-new-dialog @app-added="refreshList" @close="appDialog = false" />
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import ListItemUserApp from './ListItemUserApp'
|
||||
import AppNewDialog from './dialogs/AppNewDialog'
|
||||
|
||||
export default {
|
||||
components: { ListItemUserApp, AppNewDialog },
|
||||
data() {
|
||||
return {
|
||||
appDialog: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
apps: {
|
||||
query: gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
createdApps {
|
||||
id
|
||||
secret
|
||||
name
|
||||
description
|
||||
redirectUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.user.createdApps
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshList() {
|
||||
this.$apollo.queries.apps.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<v-card :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} mt-10`">
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Usage Stats</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-row v-if="!$apollo.loading" dense class="mt-2">
|
||||
<v-col v-for="value in graphSeries" v-if="value.data" :key="value.name" cols="12" sm="6">
|
||||
<p class="text-center caption primary--text">
|
||||
<v-icon x-small color="primary" class="mr-1">{{ icons[value.name] }}</v-icon>
|
||||
{{ capitalize(value.name.split('History')[0]) }} history
|
||||
</p>
|
||||
<apexchart class="primary--text" type="bar" :options="options" :series="[value]" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { formatNumber } from '@/formatNumber.js'
|
||||
|
||||
export default {
|
||||
name: 'ActivityCard',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
icons: {
|
||||
commitHistory: 'mdi-cloud-upload-outline',
|
||||
streamHistory: 'mdi-cloud-outline',
|
||||
objectHistory: 'mdi-cube-outline',
|
||||
userHistory: 'mdi-account-outline'
|
||||
},
|
||||
options: {
|
||||
states: {
|
||||
active: {
|
||||
filter: {
|
||||
type: 'none' /* none, lighten, darken */
|
||||
}
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
id: 'newUserData',
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
position: 'bottom',
|
||||
formatter: function (val) {
|
||||
return formatNumber(val)
|
||||
},
|
||||
offsetY: -25,
|
||||
style: {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 'bold',
|
||||
colors: undefined
|
||||
},
|
||||
background: {
|
||||
enabled: true,
|
||||
foreColor: '#fff',
|
||||
padding: 6,
|
||||
borderRadius: 5,
|
||||
borderWidth: 2,
|
||||
borderColor: undefined,
|
||||
opacity: 0.9
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
labels: {
|
||||
show: true,
|
||||
rotate: 0,
|
||||
rotateAlways: true,
|
||||
hideOverlappingLabels: true,
|
||||
showDuplicates: false,
|
||||
trim: false,
|
||||
style: {
|
||||
colors: [],
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 400,
|
||||
cssClass: 'apexcharts-xaxis-label text-center'
|
||||
},
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: false
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: '90%',
|
||||
barHeight: '10%',
|
||||
dataLabels: {
|
||||
position: 'top' // top, center, bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverStats: {
|
||||
query: gql`
|
||||
query {
|
||||
serverStats {
|
||||
commitHistory
|
||||
objectHistory
|
||||
userHistory
|
||||
streamHistory
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
var stats = data.serverStats
|
||||
delete stats.__typename
|
||||
return stats
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
graphSeries() {
|
||||
let result = []
|
||||
let months = this.past12Months()
|
||||
if (this.serverStats) {
|
||||
result = Object.keys(this.serverStats).map((key) => {
|
||||
let category = this.serverStats[key]
|
||||
let processed = []
|
||||
months?.forEach((month) => {
|
||||
let totalCount = 0
|
||||
category.forEach((value) => {
|
||||
let date = this.parseISOString(value.created_month)
|
||||
if (this.isSameMonth(month, date)) {
|
||||
totalCount = value.count
|
||||
}
|
||||
})
|
||||
processed.push([month, totalCount])
|
||||
})
|
||||
return { name: key, data: processed }
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
capitalize(word) {
|
||||
return word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
},
|
||||
past12Months() {
|
||||
let now = new Date(Date.now())
|
||||
let dates = []
|
||||
for (let i = 0; i < 12; i++) {
|
||||
let d = new Date(now.getFullYear(), now.getMonth() - i, 2)
|
||||
dates.push(d)
|
||||
}
|
||||
return dates
|
||||
},
|
||||
isSameMonth(refDate, date) {
|
||||
return (
|
||||
refDate.getUTCFullYear() === date.getUTCFullYear() &&
|
||||
refDate.getUTCMonth() === date.getUTCMonth()
|
||||
)
|
||||
},
|
||||
parseISOString(s) {
|
||||
let b = s.split(/\D+/)
|
||||
return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6]))
|
||||
},
|
||||
isoFormatDMY(d) {
|
||||
function pad(n) {
|
||||
return (n < 10 ? '0' : '') + n
|
||||
}
|
||||
return pad(d.getUTCDate()) + '/' + pad(d.getUTCMonth() + 1) + '/' + d.getUTCFullYear()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<section-card expandable>
|
||||
<template #header>General Info</template>
|
||||
<v-row class="d-flex justify-space-around mt-4">
|
||||
<v-col
|
||||
v-for="(value, name) in serverStats"
|
||||
:key="name"
|
||||
cols="6"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<h4 class="primary--text text--lighten-2 text-center">Total {{ name }}</h4>
|
||||
<v-tooltip bottom color="primary" :disabled="value < 1000">
|
||||
<template #activator="{ on, attrs }">
|
||||
<p
|
||||
class="primary--text text-h3 text-md-h2 text-lg-h1 text-center"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<animated-number :value="value" class="speckle-gradient-txt" />
|
||||
</p>
|
||||
</template>
|
||||
<span>{{ value }}</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'GeneralInfoCard',
|
||||
components: {
|
||||
AnimatedNumber: () => import('@/components/AnimatedNumber'),
|
||||
SectionCard: () => import('@/cleanup/components/common/SectionCard')
|
||||
},
|
||||
apollo: {
|
||||
serverStats: {
|
||||
query: gql`
|
||||
query {
|
||||
serverStats {
|
||||
totalObjectCount
|
||||
totalCommitCount
|
||||
totalStreamCount
|
||||
totalUserCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
var stats = data.serverStats
|
||||
return {
|
||||
users: stats.totalUserCount,
|
||||
streams: stats.totalStreamCount,
|
||||
commits: stats.totalCommitCount,
|
||||
objects: stats.totalObjectCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title>Server Admin</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<template slot="menu">
|
||||
<v-slide-x-reverse-transition mode="out-in">
|
||||
<v-tooltip v-if="!addUserMode" left color="primary" open-delay="500">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
rounded
|
||||
small
|
||||
outlined
|
||||
color="primary"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
class="mr-1"
|
||||
@click="addUserMode = true"
|
||||
>
|
||||
<v-icon small>mdi-plus</v-icon>
|
||||
Add
|
||||
</v-btn>
|
||||
</template>
|
||||
Add new admin
|
||||
</v-tooltip>
|
||||
<div v-else class="d-flex align-center">
|
||||
<v-autocomplete
|
||||
:search-input.sync="search"
|
||||
v-model="selectedSearchItem"
|
||||
:items="filteredSearchResults"
|
||||
:loading="$apollo.loading"
|
||||
autofocus
|
||||
label="Search users..."
|
||||
dense
|
||||
hide-details
|
||||
hide-no-data
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
return-object
|
||||
chips
|
||||
deletable-chips
|
||||
class="mr-2"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
<v-btn outlined small color="success" elevation="0" class="mr-2">Add</v-btn>
|
||||
<v-btn outlined small color="error" elevation="0" @click="addUserMode = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-slide-x-reverse-transition>
|
||||
</template>
|
||||
<div v-for="admin in adminUsers" :key="admin.id">
|
||||
<server-admins-user :admin="admin" />
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ServerAdminsUser from '@/components/admin/ServerAdminsUser'
|
||||
import SearchBar from '@/components/SearchBar'
|
||||
import userSearchQuery from '@/graphql/userSearch.gql'
|
||||
|
||||
export default {
|
||||
name: 'ServerAdminsCard',
|
||||
components: { SearchBar, ServerAdminsUser },
|
||||
props: ['adminUsers'],
|
||||
data() {
|
||||
return {
|
||||
selectedSearchItem: [],
|
||||
search: '',
|
||||
userSearch: { items: [] },
|
||||
addUserMode: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
userSearch: {
|
||||
query: userSearchQuery,
|
||||
variables() {
|
||||
return {
|
||||
query: this.search,
|
||||
limit: 25
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredSearchResults() {
|
||||
if (!this.userSearch) return null
|
||||
let users = []
|
||||
for (let u of this.userSearch.items) {
|
||||
if (u.id === this.myId) continue
|
||||
users.push(u)
|
||||
}
|
||||
return users
|
||||
},
|
||||
myId() {
|
||||
return localStorage.getItem('uuid')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addUser(event, user) {
|
||||
console.log('user to add as admin', event, user)
|
||||
this.addUserMode = !this.addUserMode
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<user-list-item :admin="admin" :widgets="widgets">
|
||||
<v-slide-x-reverse-transition hide-on-leave>
|
||||
<v-menu offset-y left rounded v-if="!deleteRequested">
|
||||
<template v-slot:activator="{attrs,on}">
|
||||
<v-btn icon small v-bind="attrs" v-on="on" class="ml-2">
|
||||
<v-icon small>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list nav dense>
|
||||
<v-tooltip left max-width="200pt" open-delay="500">
|
||||
<template v-slot:activator="{attrs, on}">
|
||||
<v-list-item v-bind="attrs" v-on="on" v-for="opt in menuOptions" :key="opt.text" link @click="deleteRequested = true">
|
||||
<v-list-item-icon class="mr-3">
|
||||
<v-icon color="error" small v-text="opt.icon"></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="opt.text" class="error--text"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
Removes this user's admin privileges. This <b>will not</b> delete the user's account.
|
||||
</v-tooltip>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<div v-else>
|
||||
<p class="caption mb-1 text-center">Are you sure?</p>
|
||||
<v-btn small color="success" @click="removeUserAdmin(admin)" class="mr-1">Yes</v-btn>
|
||||
<v-btn small color="error" @click="deleteRequested = !deleteRequested">Cancel</v-btn>
|
||||
</div>
|
||||
</v-slide-x-reverse-transition>
|
||||
</user-list-item>
|
||||
</template>
|
||||
<script>
|
||||
import AnimatedNumber from "@/components/AnimatedNumber";
|
||||
import UserListItem from "@/components/admin/UserListItem";
|
||||
|
||||
export default {
|
||||
name: "server-admins-user",
|
||||
components: { UserListItem, AnimatedNumber },
|
||||
props: {
|
||||
admin: {}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
deleteRequested: false,
|
||||
menuOptions: [
|
||||
{
|
||||
text: "Remove",
|
||||
icon: "mdi-delete"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
widgets(){
|
||||
return [
|
||||
{
|
||||
icon: 'mdi-eye',
|
||||
hint: 'Last seen',
|
||||
value: '< 1 day',
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeUserAdmin(admin) {
|
||||
console.log("Requested removal of user from admin scope", admin);
|
||||
this.deleteRequested = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.admin-user-view {
|
||||
border-bottom: 1pt dotted var(--v-background-darken1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="d-flex align-center pa-3 admin-user-view">
|
||||
<div class="d-flex flex-grow-1 align-center">
|
||||
<img
|
||||
height="60pt"
|
||||
width="60pt"
|
||||
class="rounded-circle overflow-hidden elevation-1"
|
||||
contain
|
||||
:src="admin.avatar"
|
||||
/>
|
||||
<div class="d-flex flex-column flex-grow-1 ml-2" style="min-width: 30%">
|
||||
<span class="subtitle-1">{{ admin.name }}</span>
|
||||
<span class="caption">
|
||||
<v-icon x-small>mdi-email-outline</v-icon>
|
||||
{{ admin.email }}
|
||||
<v-tooltip right :color="admin.verified ? 'success' : 'error'" class="caption">
|
||||
<template v-slot:activator="{attrs, on}">
|
||||
<v-icon v-bind="attrs" v-on="on" small :color="admin.verified ? 'success' : 'error'">{{admin.verified ? 'mdi-check-decagram' : 'mdi-alert-decagram'}}</v-icon>
|
||||
</template>
|
||||
<span class="caption">{{admin.verified ? 'Verified' : 'Pending verification'}}</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
<span class="caption">
|
||||
<v-icon x-small>mdi-domain</v-icon>
|
||||
{{ admin.company }}
|
||||
</span>
|
||||
</div>
|
||||
<slot name="mid"></slot>
|
||||
<div v-if="widgets" class="d-flex widget-parent">
|
||||
<div v-for="(col, index) in organizedWidgets" :key="index" class="d-flex flex-column align-content-start justify-center pr-2">
|
||||
<v-tooltip v-for="widget in col" left color="primary" :key="widget.text">
|
||||
<template v-slot:activator="{attrs, on}">
|
||||
<span v-bind="attrs"
|
||||
v-on="on"
|
||||
class="caption" :class="(widget.color || 'dark') + '--text'">
|
||||
<v-icon small class="pr-1" :class="(widget.color || 'dark') + '--text'">{{widget.icon}}</v-icon>
|
||||
<animated-number v-if="widget.type === 'number'" :value="widget.value"/>
|
||||
<span v-else>{{ widget.value }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<span class="caption">
|
||||
{{ widget.hint }}
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import AnimatedNumber from "@/components/AnimatedNumber";
|
||||
export default {
|
||||
name: "user-list-item",
|
||||
components: { AnimatedNumber },
|
||||
props: {
|
||||
admin: {},
|
||||
widgets: {},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
organizedWidgets(){
|
||||
var cols = []
|
||||
for (let i = 0; i < this.widgets.length; i+=3) {
|
||||
var row =[]
|
||||
for (let j = 0; j < 3; j++) {
|
||||
var index = i + j;
|
||||
if(index >= this.widgets.length) break;
|
||||
row.push(this.widgets[index])
|
||||
}
|
||||
cols.push(row)
|
||||
}
|
||||
|
||||
return cols
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.admin-user-view {
|
||||
border-bottom: 1pt dotted var(--v-background-darken1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<v-card :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} mt-10`">
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Version Info</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon href="https://github.com/specklesystems/speckle-server/releases" target="_blank">
|
||||
<span v-if="isLatestVersion" class="text--h6 success--text">
|
||||
<v-icon v-tooltip="'Up to date.'" size="medium" color="success">mdi-check-bold</v-icon>
|
||||
</span>
|
||||
<span v-else class="warning--text">
|
||||
<v-icon v-tooltip="'There is a newer version available!'" size="medium" color="warning">
|
||||
mdi-alert
|
||||
</v-icon>
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<div class="d-flex justify-space-around pl-4 pr-4 mt-4">
|
||||
<div>
|
||||
<h4 class="primary--text text--lighten-2">Current</h4>
|
||||
<p class="primary--text text-h4 text-sm-h2 speckle-gradient-txt">
|
||||
{{ versionInfo.current }}
|
||||
</p>
|
||||
</div>
|
||||
<v-icon color="primary lighten-1">mdi-arrow-right</v-icon>
|
||||
<div>
|
||||
<h4 class="primary--text text--lighten-2">Latest</h4>
|
||||
<p class="primary--text text-h4 text-sm-h2 speckle-gradient-txt">
|
||||
{{ versionInfo.latest }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'VersionInfoCard',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
versionInfo: {
|
||||
current: '2.0.18',
|
||||
latest: '2.0.27'
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
currentVersion: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
version
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
this.versionInfo.current = data.serverInfo.version
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLatestVersion() {
|
||||
return this.versionInfo.current === this.versionInfo.latest
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.versionInfo.latest = await this.getLatestVersion()
|
||||
},
|
||||
methods: {
|
||||
getLatestVersion() {
|
||||
return fetch('https://api.github.com/repos/specklesystems/speckle-server/releases/latest')
|
||||
.then(async (res) => {
|
||||
var x = await res.json()
|
||||
return x.tag_name
|
||||
})
|
||||
.catch((err) => console.error('error fetch', err))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -4,6 +4,10 @@ Vue.prototype.$userId = function () {
|
||||
return localStorage.getItem('uuid')
|
||||
}
|
||||
|
||||
Vue.prototype.$mixpanelId = function () {
|
||||
return localStorage.getItem('distinct_id')
|
||||
}
|
||||
|
||||
Vue.prototype.$loggedIn = function () {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
<template>
|
||||
<v-app id="speckle">
|
||||
<v-navigation-drawer
|
||||
:app="!$vuetify.breakpoint.xsOnly"
|
||||
permanent
|
||||
mini-variant
|
||||
:expand-on-hover="true || $vuetify.breakpoint.mdAndUp"
|
||||
floating
|
||||
stateless
|
||||
fixed
|
||||
:color="`${$vuetify.theme.dark ? 'grey darken-4' : 'grey lighten-4'}`"
|
||||
:dark="$vuetify.theme.dark"
|
||||
style="z-index: 100"
|
||||
:class="`elevation-5 hidden-xs-only`"
|
||||
mini-variant-width="56"
|
||||
>
|
||||
<v-toolbar class="transparent elevation-0">
|
||||
<v-toolbar-title class="space-grotesk primary--text">
|
||||
<router-link to="/" class="text-decoration-none">
|
||||
<v-img
|
||||
class="mt-2 hover-tada"
|
||||
width="24"
|
||||
src="@/assets/specklebrick.png"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/"
|
||||
class="text-decoration-none"
|
||||
style="position: relative; top: -4px; margin-left: 20px"
|
||||
>
|
||||
<span class="pb-4"><b>Speckle</b></span>
|
||||
</router-link>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list>
|
||||
<v-list-item link to="/" style="height: 59px">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-clock-fast</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Feed</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Latest events.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/streams" style="height: 59px">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-folder</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Streams</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">All your streams.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="user" link to="/profile" style="height: 59px">
|
||||
<v-list-item-icon>
|
||||
<user-avatar-icon :size="24" :avatar="user.avatar" :seed="user.id"></user-avatar-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Profile</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Your profile & dev settings.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item v-if="serverInfo">
|
||||
<v-list-item-icon>
|
||||
<v-icon
|
||||
v-if="isDevServer"
|
||||
v-tooltip="`This is a test server and should not be used in production!`"
|
||||
color="red"
|
||||
>
|
||||
mdi-alert
|
||||
</v-icon>
|
||||
<v-icon v-else>mdi-information-variant</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="caption">{{ serverInfo.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ serverInfo.version }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="caption">
|
||||
{{ serverInfo.description }}
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template #append>
|
||||
<v-list dense>
|
||||
<v-list-item
|
||||
link
|
||||
href="https://speckle.community/new-topic?category=features"
|
||||
target="_blank"
|
||||
class="primary"
|
||||
dark
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-comment-arrow-right</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Feedback</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="user" link color="primary" @click="signOut()">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-account-off</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Logout</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Server Admin</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link @click="switchTheme">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-theme-light-dark</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Switch Theme</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-bottom-navigation fixed xxx-hide-on-scroll class="hidden-sm-and-up elevation-20">
|
||||
<v-btn color="primary" text to="/" style="height: 100%">
|
||||
<span>Feed</span>
|
||||
<v-icon>mdi-clock-fast</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" text to="/streams" style="height: 100%">
|
||||
<span>Streams</span>
|
||||
<v-icon>mdi-folder</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" text to="/profile" style="height: 100%">
|
||||
<span>Profile</span>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn text style="height: 100%" @click="bottomSheet = true">
|
||||
<span>More</span>
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
|
||||
<v-bottom-sheet v-model="bottomSheet">
|
||||
<v-sheet class="">
|
||||
<v-toolbar class="transparent elevation-0">
|
||||
<v-toolbar-title class="space-grotesk primary--text">
|
||||
<router-link to="/" class="text-decoration-none">
|
||||
<v-img
|
||||
class="mt-2"
|
||||
max-width="30"
|
||||
src="@/assets/logo.svg"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/"
|
||||
class="text-decoration-none"
|
||||
style="position: relative; top: -4px; margin-left: 20px"
|
||||
>
|
||||
<span class="pb-4"><b>Speckle</b></span>
|
||||
</router-link>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
link
|
||||
href="https://speckle.community/new-topic?category=features"
|
||||
target="_blank"
|
||||
class="primary"
|
||||
dark
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-comment-arrow-right</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Feedback</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="user" link color="primary" @click="signOut()">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-account-off</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Logout</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Server Admin</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link @click="switchTheme">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-theme-light-dark</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Switch Theme</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="serverInfo">
|
||||
<v-list-item-icon>
|
||||
<v-icon
|
||||
v-if="serverInfo && isDevServer"
|
||||
v-tooltip="`This is a test server and should not be used in production!`"
|
||||
color="red"
|
||||
>
|
||||
mdi-alert
|
||||
</v-icon>
|
||||
<v-icon v-else>mdi-information-variant</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="caption">{{ serverInfo.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ serverInfo.version }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="caption">This is a test server and should not be used in production!</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-sheet>
|
||||
</v-bottom-sheet>
|
||||
<v-main style="overflow: hidden">
|
||||
<transition name="fade">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
</v-main>
|
||||
<v-snackbar
|
||||
v-model="streamSnackbar"
|
||||
rounded="pill"
|
||||
:timeout="10000"
|
||||
style="z-index: 10000"
|
||||
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
|
||||
>
|
||||
<span v-if="streamSnackbarInfo.sharedBy">You have been granted access to a new stream!</span>
|
||||
<span v-else>New stream created!</span>
|
||||
<template #action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="goToStreamAndCloseSnackbar()">View</v-btn>
|
||||
<v-btn color="pink" icon v-bind="attrs" @click="streamSnackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { signOut } from '@/auth-helpers'
|
||||
import userQuery from '../graphql/user.gql'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
inject: ['mixpanel'],
|
||||
data() {
|
||||
return {
|
||||
streamSnackbar: false,
|
||||
streamSnackbarInfo: {},
|
||||
bottomSheet: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
version
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
user: {
|
||||
query: userQuery,
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
userStreamAdded: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamAdded
|
||||
}
|
||||
`,
|
||||
result(streamInfo) {
|
||||
console.log(streamInfo)
|
||||
if (!streamInfo.data.userStreamAdded) return
|
||||
this.streamSnackbar = true
|
||||
this.streamSnackbarInfo = streamInfo.data.userStreamAdded
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
background() {
|
||||
let theme = this.$vuetify.theme.dark ? 'dark' : 'light'
|
||||
return `background-color: ${this.$vuetify.theme.themes[theme].background};`
|
||||
},
|
||||
isDevServer() {
|
||||
return this.serverInfo.version.match(/^[0-9]+\.[0-9]+\.[0-9]+$/) ? false : true
|
||||
},
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to) {
|
||||
this.bottomSheet = false
|
||||
// close the snackbar if it's a stream create event in this window
|
||||
if (to.params.streamId === this.streamSnackbarInfo.id) this.streamSnackbar = false
|
||||
this.bottomSheet = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signOut() {
|
||||
signOut()
|
||||
},
|
||||
switchTheme() {
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
localStorage.setItem('darkModeEnabled', this.$vuetify.theme.dark ? 'dark' : 'light')
|
||||
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light')
|
||||
},
|
||||
showStreamInviteDialog() {
|
||||
this.$refs.streamInviteDialog.show()
|
||||
},
|
||||
goToStreamAndCloseSnackbar() {
|
||||
this.streamSnackbar = false
|
||||
this.$router.push(`/streams/${this.streamSnackbarInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.space-grotesk {
|
||||
font-family: 'Space Grotesk' !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
text-transform: none;
|
||||
color: rgb(37, 99, 235);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.no-hover:before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<v-container
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`"
|
||||
>
|
||||
<!-- <v-container :fluid="$vuetify.breakpoint.mdAndDown"> -->
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
:style="`margin-top: ${$vuetify.breakpoint.smAndDown ? '0px' : '50px'}`"
|
||||
class="pa-3"
|
||||
>
|
||||
<user-info-card :user="user" @update="update"></user-info-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<user-authorised-apps />
|
||||
<v-alert type="info" class="my-5 mt-10 mx-4">
|
||||
Heads up! The sections below are intended for developers.
|
||||
</v-alert>
|
||||
<v-card color="transparent" class="elevation-0 mt-3">
|
||||
<v-card-title>
|
||||
Trying to learn the api?
|
||||
<v-spacer />
|
||||
<v-btn href="/explorer" text color="primary" target="_blank">
|
||||
Checkout the GraphIQL explorer!
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-card color="transparent" flat>
|
||||
<v-card-text>
|
||||
<user-access-tokens />
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<user-apps />
|
||||
<user-delete-card :user="user" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import userQuery from '../graphql/user.gql'
|
||||
import UserInfoCard from '../components/UserInfoCard'
|
||||
import UserAccessTokens from '../components/UserAccessTokens'
|
||||
import UserApps from '../components/UserApps'
|
||||
import UserAuthorisedApps from '../components/UserAuthorisedApps'
|
||||
import UserDeleteCard from '../components/UserDeleteCard'
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
components: {
|
||||
UserInfoCard,
|
||||
UserAccessTokens,
|
||||
UserApps,
|
||||
UserAuthorisedApps,
|
||||
UserDeleteCard
|
||||
},
|
||||
data: () => ({}),
|
||||
apollo: {
|
||||
user: {
|
||||
query: userQuery
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
update() {
|
||||
this.$apollo.queries.user.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-item-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<!-- <v-container :fluid="$vuetify.breakpoint.mdAndDown"> -->
|
||||
<v-container :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`" >
|
||||
<v-row v-if="$apollo.loading">
|
||||
<v-col cols="12">
|
||||
<v-skeleton-loader type="card, article"></v-skeleton-loader>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<user-info-card :user="user"></user-info-card>
|
||||
</v-col>
|
||||
<v-col cols="12" class="pt-10">
|
||||
<v-card class="mb-3 elevation-0">
|
||||
<v-card-title>
|
||||
{{ user.name }} and you share {{ user.streams.totalCount }}
|
||||
{{ user.streams.totalCount === 1 ? 'stream' : 'streams' }}. {{ user.name.split(' ')[0] }} has
|
||||
{{ user.commits.totalCount }}
|
||||
{{ user.commits.totalCount === 1 ? 'commit' : 'commits' }}.
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-row>
|
||||
<v-col v-for="(stream, i) in user.streams.items" :key="i" cols="12" sm="6" lg="4">
|
||||
<list-item-stream :stream="stream"></list-item-stream>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import UserInfoCard from '../components/UserInfoCard'
|
||||
import ListItemStream from '../components/ListItemStream'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'ProfileUser',
|
||||
components: { UserInfoCard, ListItemStream },
|
||||
data: () => ({}),
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
query User($id: String!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
suuid
|
||||
streams {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
description
|
||||
updatedAt
|
||||
name
|
||||
}
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.userId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
// Move to self profile
|
||||
if (this.$route.params.userId === localStorage.getItem('uuid')) {
|
||||
this.$router.replace({ path: '/profile' })
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-item-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
@@ -1,302 +0,0 @@
|
||||
<template>
|
||||
<v-container
|
||||
:class="`${$vuetify.breakpoint.xsOnly ? 'pl-2' : ''}`"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
fluid
|
||||
pt-4
|
||||
pr-0
|
||||
>
|
||||
<v-navigation-drawer
|
||||
v-model="streamNav"
|
||||
app
|
||||
fixed
|
||||
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<main-nav-actions :open-new-stream="newStreamDialog" />
|
||||
|
||||
<div v-if="user">
|
||||
<v-list dense class="py-0">
|
||||
<v-subheader class="caption ml-2">Your stats:</v-subheader>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="">mdi-folder-multiple</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ user.streams.totalCount }}</b>
|
||||
total streams
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="">mdi-source-commit</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ user.commits.totalCount }}</b>
|
||||
total commits
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-list
|
||||
v-if="userCommits && userCommits.commits.items.length !== 0"
|
||||
color="transparent"
|
||||
dense
|
||||
class="py-0"
|
||||
>
|
||||
<v-subheader class="ml-2">Your latest commits:</v-subheader>
|
||||
<v-list-item
|
||||
v-for="(commit, i) in userCommits.commits.items"
|
||||
v-if="commit"
|
||||
:key="i"
|
||||
v-tooltip="`In stream '${commit.streamName}'`"
|
||||
:to="`streams/${commit.streamId}/${
|
||||
commit.branchName === 'globals' ? 'globals' : 'commits'
|
||||
}/${commit.id}`"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ commit.message }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<i>
|
||||
Updated
|
||||
<timeago :datetime="commit.createdAt"></timeago>
|
||||
</i>
|
||||
on
|
||||
<v-icon style="font-size: 10px">mdi-source-branch</v-icon>
|
||||
{{ commit.branchName }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat>
|
||||
<v-app-bar-nav-icon @click="streamNav = !streamNav"></v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="space-grotesk pl-0">
|
||||
<v-icon class="mb-1 hidden-xs-only">mdi-folder</v-icon>
|
||||
Streams
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items v-if="$vuetify.breakpoint.smAndDown" style="margin-right: -18px">
|
||||
<v-btn color="primary" depressed @click="newStreamDialog++">
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- <getting-started-wizard /> -->
|
||||
|
||||
<v-row class="pl-2 pr-4" no-gutters>
|
||||
<v-col v-if="$apollo.loading">
|
||||
<v-row>
|
||||
<v-col v-for="i in 6" :key="i" cols="12" sm="6" md="6" lg="4" xl="4">
|
||||
<v-skeleton-loader type="card, list-item-two-line" class="ma-2"></v-skeleton-loader>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="$apollo.loading" class="my-5"></div>
|
||||
</v-col>
|
||||
|
||||
<v-col v-else-if="streams && streams.items && streams.items.length > 0" cols="12">
|
||||
<v-row :class="`${$vuetify.breakpoint.xsOnly ? '' : 'pl-2'}`">
|
||||
<v-col
|
||||
v-for="(stream, i) in streams.items"
|
||||
:key="i"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="4"
|
||||
>
|
||||
<list-item-stream :stream="stream"></list-item-stream>
|
||||
</v-col>
|
||||
<infinite-loading
|
||||
v-if="streams.items.length < streams.totalCount"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">These are all your streams!</div>
|
||||
<div slot="no-results">There are no streams to load</div>
|
||||
</infinite-loading>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col v-else cols="12">
|
||||
<no-data-placeholder v-if="user">
|
||||
<h2>Welcome {{ user.name.split(' ')[0] }}!</h2>
|
||||
<p class="caption">
|
||||
Once you will create a stream and start sending some data, your activity will show up
|
||||
here.
|
||||
</p>
|
||||
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item link class="primary mb-4" dark @click="newStreamDialog++">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Create a new stream!</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Streams are like folders, or data repositories.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</no-data-placeholder>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import streamsQuery from '../graphql/streams.gql'
|
||||
import userQuery from '../graphql/user.gql'
|
||||
|
||||
export default {
|
||||
name: 'Streams',
|
||||
components: {
|
||||
InfiniteLoading: () => import('vue-infinite-loading'),
|
||||
ListItemStream: () => import('@/components/ListItemStream'),
|
||||
MainNavActions: () => import('@/components/MainNavActions'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder')
|
||||
},
|
||||
apollo: {
|
||||
streams: {
|
||||
prefetch: true,
|
||||
query: streamsQuery,
|
||||
fetchPolicy: 'cache-and-network' //https://www.apollographql.com/docs/react/data/queries/
|
||||
},
|
||||
user: {
|
||||
query: userQuery,
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
userCommits: {
|
||||
query: gql`
|
||||
query {
|
||||
userCommits: user {
|
||||
id
|
||||
commits(limit: 7) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
message
|
||||
sourceApplication
|
||||
streamId
|
||||
streamName
|
||||
branchName
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
userStreamAdded: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamAdded
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
userStreamRemoved: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamRemoved
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
streams: [],
|
||||
streamNav: true,
|
||||
newStreamDialog: 0
|
||||
}),
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
streams(val) {
|
||||
if (val.items.length === 0 && !localStorage.getItem('onboarding')) {
|
||||
this.$router.push('/onboarding')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
function () {
|
||||
this.streamNav = !this.$vuetify.breakpoint.smAndDown
|
||||
}.bind(this),
|
||||
100
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
showServerInviteDialog() {
|
||||
this.$refs.serverInviteDialog.show()
|
||||
},
|
||||
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.streams.fetchMore({
|
||||
variables: {
|
||||
cursor: this.streams.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.streams.items
|
||||
|
||||
//set vue-infinite state
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
streams: {
|
||||
__typename: previousResult.streams.__typename,
|
||||
totalCount: fetchMoreResult.streams.totalCount,
|
||||
cursor: fetchMoreResult.streams.cursor,
|
||||
// Merging the new streams
|
||||
items: [...previousResult.streams.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.recent-commits a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recent-commits a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -1,251 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<portal to="title">
|
||||
<div class="font-weight-bold">Feed</div>
|
||||
</portal>
|
||||
<v-row class="pr-4">
|
||||
<v-col v-if="$apollo.loading && !timeline">
|
||||
<div class="my-5">
|
||||
<v-timeline align-top dense>
|
||||
<v-timeline-item v-for="i in 6" :key="i" medium>
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-else-if="timeline && timeline.items.length > 0"
|
||||
cols="12"
|
||||
lg="8"
|
||||
class="pr-2"
|
||||
:style="`${$vuetify.breakpoint.xsOnly ? 'margin-left: -20px;' : ''}`"
|
||||
>
|
||||
<div>
|
||||
<div v-if="timeline" key="activity-list">
|
||||
<v-timeline align-top dense>
|
||||
<list-item-activity
|
||||
v-for="activity in groupedTimeline"
|
||||
:key="activity.time"
|
||||
:activity="activity"
|
||||
:activity-group="activity"
|
||||
class="my-1"
|
||||
></list-item-activity>
|
||||
<infinite-loading
|
||||
v-if="timeline && timeline.items.length < timeline.totalCount"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">This is all your activity!</div>
|
||||
<div slot="no-results">There are no ctivities to load</div>
|
||||
</infinite-loading>
|
||||
</v-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col v-else cols="12">
|
||||
<no-data-placeholder v-if="quickUser">
|
||||
<h2>Welcome {{ quickUser.name.split(' ')[0] }}!</h2>
|
||||
<p class="caption">
|
||||
Once you will create a stream and start sending some data, your activity will show up
|
||||
here.
|
||||
</p>
|
||||
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item
|
||||
link
|
||||
class="primary mb-4"
|
||||
dark
|
||||
@click="$eventHub.$emit('show-new-stream-dialog')"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Create a new stream!</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Streams are like folders, or data repositories.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</no-data-placeholder>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-show="$vuetify.breakpoint.lgAndUp"
|
||||
v-if="timeline && timeline.items.length > 0"
|
||||
cols="12"
|
||||
lg="4"
|
||||
class="mt-7"
|
||||
style="position: sticky; top: 64px"
|
||||
>
|
||||
<div class="sticky-top">
|
||||
<latest-blogposts></latest-blogposts>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'Timeline',
|
||||
components: {
|
||||
InfiniteLoading: () => import('vue-infinite-loading'),
|
||||
ListItemActivity: () => import('@/components/ListItemActivity'),
|
||||
LatestBlogposts: () => import('@/components/LatestBlogposts'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder')
|
||||
},
|
||||
props: {
|
||||
type: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newStreamDialog: 0,
|
||||
activityNav: true
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
quickUser: {
|
||||
query: gql`
|
||||
query {
|
||||
quickUser: user {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
timeline: {
|
||||
query: gql`
|
||||
query($cursor: DateTime) {
|
||||
user {
|
||||
id
|
||||
timeline(cursor: $cursor) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
actionType
|
||||
userId
|
||||
streamId
|
||||
resourceId
|
||||
resourceType
|
||||
time
|
||||
info
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
update(data) {
|
||||
return data.user.timeline
|
||||
},
|
||||
result({ data }) {
|
||||
this.groupSimilarActivities(data)
|
||||
}
|
||||
},
|
||||
streams: {
|
||||
prefetch: true,
|
||||
query: gql`
|
||||
query {
|
||||
streams(limit: 10) {
|
||||
items {
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
timeline(val) {
|
||||
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
|
||||
this.$router.push('/onboarding')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
function () {
|
||||
this.activityNav = !this.$vuetify.breakpoint.smAndDown
|
||||
}.bind(this),
|
||||
10
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
groupSimilarActivities(data) {
|
||||
if (!data) return
|
||||
let groupedTimeline = data.user.timeline.items.reduce(function (prev, curr) {
|
||||
//first item
|
||||
if (!prev.length) {
|
||||
prev.push([curr])
|
||||
return prev
|
||||
}
|
||||
let test = prev[prev.length - 1][0]
|
||||
let action = 'split' // split | combine | skip
|
||||
if (curr.actionType === test.actionType && curr.streamId === test.streamId) {
|
||||
if (curr.actionType.includes('stream_permissions')) {
|
||||
//skip multiple stream_permission actions on the same user, just pick the last!
|
||||
if (prev[prev.length - 1].some((x) => x.info.targetUser === curr.info.targetUser))
|
||||
action = 'skip'
|
||||
else action = 'combine'
|
||||
} //stream, branch, commit
|
||||
else if (curr.actionType.includes('_update') || curr.actionType === 'commit_create')
|
||||
action = 'combine'
|
||||
}
|
||||
if (action === 'combine') {
|
||||
prev[prev.length - 1].push(curr)
|
||||
} else if (action === 'split') {
|
||||
prev.push([curr])
|
||||
}
|
||||
return prev
|
||||
}, [])
|
||||
// console.log(groupedTimeline)
|
||||
this.groupedTimeline = groupedTimeline
|
||||
},
|
||||
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.timeline.fetchMore({
|
||||
variables: {
|
||||
cursor: this.timeline.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.user.timeline.items
|
||||
|
||||
//set vue-infinite state
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
fetchMoreResult.user.timeline.items = [...previousResult.user.timeline.items, ...newItems]
|
||||
|
||||
return fetchMoreResult
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sticky-top {
|
||||
position: sticky;
|
||||
top: 68px;
|
||||
}
|
||||
.recent-streams a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recent-streams .v-list-item__title {
|
||||
font-family: 'Space Grotesk' !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,135 +0,0 @@
|
||||
<template lang="html">
|
||||
<v-container v-if="$apollo.loading" fluid>
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</v-container>
|
||||
|
||||
<v-container v-else-if="isAdmin" :class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`">
|
||||
<v-navigation-drawer
|
||||
v-model="adminNav"
|
||||
app
|
||||
fixed
|
||||
:permanent="adminNav && !$vuetify.breakpoint.smAndDown"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<v-app-bar style="position: absolute; top: 0; width: 100%; z-index: 90" elevation="0">
|
||||
<v-toolbar-title>Server Admin</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-list style="margin-top: 64px; padding-left: 10px" rounded>
|
||||
<v-list-item link to="/admin/dashboard">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Various server stats at a glance.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-tune</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Edit various server settings.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/users">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-account-group</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Users</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Edit server user details.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/invites">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-account-multiple-plus-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Server invites</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Manage server invitations.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/streams">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-blur</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Streams</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Manage streams.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar
|
||||
v-if="!adminNav"
|
||||
app
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
flat
|
||||
>
|
||||
<v-app-bar-nav-icon v-if="!adminNav" @click="adminNav = !adminNav" />
|
||||
<v-toolbar-title v-if="!adminNav">Server Admin</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-container
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`"
|
||||
fluid
|
||||
pt-4
|
||||
pr-0
|
||||
>
|
||||
<transition name="fade">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
</v-container>
|
||||
</v-container>
|
||||
<v-container v-else-if="!isAdmin">
|
||||
<error-placeholder error-type="access">
|
||||
<h2>Only server admins have access to this section.</h2>
|
||||
</error-placeholder>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'AdminPanel',
|
||||
components: {
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
adminNav: true
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
query {
|
||||
user {
|
||||
role
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
prefetch: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAdmin() {
|
||||
return this.user?.role === 'server:admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Send invites to multiple adresses</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert v-model="success" prominent timeout="3000" dismissible type="success">
|
||||
Great! All invites were sent.
|
||||
</v-alert>
|
||||
<v-alert v-show="errors.length !== 0" prominent dismissible type="error">
|
||||
<p>Invite send failed for adresses:</p>
|
||||
<ul>
|
||||
<li v-for="error in errors" :key="error.email">{{ error.email }}: {{ error.reason }}</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-show="errors.length !== 0 && sentToEmails.length !== 0"
|
||||
prominent
|
||||
timeout="3000"
|
||||
dismissible
|
||||
type="success"
|
||||
>
|
||||
<p>Invite sent to: {{ sentToEmails.join(', ') }}</p>
|
||||
</v-alert>
|
||||
<v-form v-model="valid" @submit.prevent="submit">
|
||||
<v-textarea
|
||||
v-model="invitation"
|
||||
label="Invitation message"
|
||||
rounded
|
||||
filled
|
||||
auto-grow
|
||||
:rules="validation.messageRules"
|
||||
></v-textarea>
|
||||
<v-combobox
|
||||
v-model="chips"
|
||||
:search-input.sync="emails"
|
||||
placeholder="Type emails separated by commas or paste the content of a .csv"
|
||||
deletable-chips
|
||||
append-icon=""
|
||||
filled
|
||||
rounded
|
||||
flat
|
||||
type="email"
|
||||
class="lighten-2"
|
||||
:error-messages="inputErrors"
|
||||
multiple
|
||||
append-outer-icon="mdi-close"
|
||||
@keydown="keyDownHandler"
|
||||
@blur="validateAndCreateChips"
|
||||
@paste="validateAndCreateChips"
|
||||
@click:append-outer="chips = []"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="data.item"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="remove(data.item)"
|
||||
>
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<p v-if="!selectedStream">Optionaly invite users to stream.</p>
|
||||
<stream-search-bar
|
||||
v-if="!selectedStream"
|
||||
:gotostreamonclick="false"
|
||||
class="py-3"
|
||||
@select="setStream"
|
||||
/>
|
||||
<v-alert v-else text dense type="info" dismissible @input="dismiss">
|
||||
They will be invited to be collaborators on {{ selectedStream.name }} stream.
|
||||
</v-alert>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!submitable" color="primary" type="submit">Invite</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-overlay absolute :value="submitting">
|
||||
<v-progress-circular :width="1.5" indeterminate></v-progress-circular>
|
||||
</v-overlay>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
import StreamSearchBar from '@/components/SearchBar'
|
||||
import { isEmailValid } from '@/auth-helpers'
|
||||
|
||||
export default {
|
||||
name: 'AdminInvites',
|
||||
components: { StreamSearchBar },
|
||||
data() {
|
||||
return {
|
||||
valid: false,
|
||||
success: false,
|
||||
showError: false,
|
||||
errors: [],
|
||||
sentToEmails: [],
|
||||
submitting: false,
|
||||
invitation: '',
|
||||
emails: '',
|
||||
chips: [],
|
||||
inputErrors: [],
|
||||
selectedStream: null,
|
||||
validation: {
|
||||
messageRules: [
|
||||
(v) => {
|
||||
if (v.length >= 1024) return 'Message too long!'
|
||||
return true
|
||||
},
|
||||
(v) => {
|
||||
let pure = DOMPurify.sanitize(v)
|
||||
if (pure !== v) return 'No crazy hacks please.'
|
||||
else return true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submitable() {
|
||||
return this.chips && this.chips.length !== 0
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
prefetch: true
|
||||
},
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
canonicalUrl
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setStream(stream) {
|
||||
this.selectedStream = stream
|
||||
},
|
||||
dismiss() {
|
||||
this.selectedStream = null
|
||||
},
|
||||
remove(item) {
|
||||
this.chips.splice(this.chips.indexOf(item), 1)
|
||||
},
|
||||
keyDownHandler(val) {
|
||||
if (!(val.key === ' ' || val.key === ',' || val.key === 'Enter')) return
|
||||
this.validateAndCreateChips()
|
||||
},
|
||||
validateAndCreateChips() {
|
||||
this.inputErrors = []
|
||||
if (!this.emails || this.emails === '') return
|
||||
let splitEmails = this.emails.split(/[ ,]+/)
|
||||
for (let email of splitEmails) {
|
||||
let valid = isEmailValid(email) && this.chips.indexOf(email) === -1
|
||||
if (valid) {
|
||||
this.chips.push(email)
|
||||
} else {
|
||||
this.inputErrors.push('Invalid email')
|
||||
}
|
||||
}
|
||||
this.emails = ''
|
||||
},
|
||||
createInviteMessage() {
|
||||
let message =
|
||||
`You have been invited to a Speckle server: ${this.serverInfo.name} ` +
|
||||
`by ${this.user.name}. Visit ${this.serverInfo.canonicalUrl} to register.`
|
||||
return this.invitation || message
|
||||
},
|
||||
async submit() {
|
||||
this.submitting = true
|
||||
this.errors = []
|
||||
this.sentToEmails = []
|
||||
for (let chip of this.chips) {
|
||||
if (!chip || chip.length === 0) continue
|
||||
try {
|
||||
await this.sendInvite(chip, this.createInviteMessage(), this.selectedStream?.id)
|
||||
this.sentToEmails.push(chip)
|
||||
} catch (err) {
|
||||
this.errors.push({ email: chip, reason: err.graphQLErrors[0].message })
|
||||
}
|
||||
}
|
||||
|
||||
this.submitting = false
|
||||
if (this.errors.length === 0) {
|
||||
this.success = true
|
||||
this.chips = []
|
||||
this.dismiss()
|
||||
}
|
||||
},
|
||||
async sendInvite(email, message, streamId) {
|
||||
let input = {
|
||||
email: email,
|
||||
message: message
|
||||
}
|
||||
|
||||
let query = gql`
|
||||
mutation($input: ${streamId ? 'StreamInviteCreateInput!' : 'ServerInviteCreateInput!'}) {
|
||||
${streamId ? 'streamInviteCreate' : 'serverInviteCreate'}(input: $input)
|
||||
}
|
||||
`
|
||||
if (streamId) {
|
||||
input.streamId = streamId
|
||||
}
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: query,
|
||||
variables: {
|
||||
input: input
|
||||
}
|
||||
})
|
||||
|
||||
this.$matomo && this.$matomo.trackEvent('invite', 'server')
|
||||
this.$mixpanel.track('Invite Send', { type: 'action', source: 'admin', hostApp: 'web' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div id="admin-overview">
|
||||
<component v-for="comp in cards" :key="comp.name" :is="comp" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GeneralInfoCard from '@/components/admin/GeneralInfoCard'
|
||||
import VersionInfoCard from '@/components/admin/VersionInfoCard'
|
||||
import ActivityCard from '@/components/admin/ActivityCard'
|
||||
|
||||
export default {
|
||||
name: 'AdminOverview',
|
||||
data() {
|
||||
return {
|
||||
cards: [GeneralInfoCard, ActivityCard, VersionInfoCard]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#admin-overview {
|
||||
.v-card:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<div id="admin-settings">
|
||||
<v-card v-if="serverInfo" rounded="lg">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar-title>{{ serverInfo.name }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<div key="viewPanel">
|
||||
<div v-for="(value, name) in serverDetails" :key="name" class="d-flex align-center mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<div v-if="value.type == 'boolean'">
|
||||
<p class="mt-2">{{ value.label }}</p>
|
||||
<v-switch
|
||||
v-model="serverModifications[name]"
|
||||
inset
|
||||
persistent-hint
|
||||
class="pa-1 ma-1 caption"
|
||||
>
|
||||
<template #label>
|
||||
<span class="caption">{{ value.hint }}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="serverModifications[name]"
|
||||
persistent-hint
|
||||
:hint="value.hint"
|
||||
class="ma-0 body-2"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn block color="primary" :loading="loading" @click="saveEdit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'ServerInfoAdminCard',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
edit: false,
|
||||
loading: false,
|
||||
serverModifications: {},
|
||||
serverDetails: {
|
||||
name: {
|
||||
label: 'Name',
|
||||
hint: "This server's public name"
|
||||
},
|
||||
description: {
|
||||
label: 'Description',
|
||||
hint: 'A short description of this server'
|
||||
},
|
||||
company: {
|
||||
label: 'Company',
|
||||
hint: 'The owner of this server'
|
||||
},
|
||||
adminContact: {
|
||||
label: 'Admin contact',
|
||||
hint: 'The administrator of this server'
|
||||
},
|
||||
termsOfService: {
|
||||
label: 'Terms of service',
|
||||
hint: 'Url pointing to the terms of service page'
|
||||
},
|
||||
inviteOnly: {
|
||||
label: 'Invite-Only mode',
|
||||
hint: 'Only users with an invitation will be able to join',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
termsOfService
|
||||
inviteOnly
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
delete data.serverInfo.__typename
|
||||
this.serverModifications = Object.assign({}, data.serverInfo)
|
||||
return data.serverInfo
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async saveEdit() {
|
||||
this.loading = true
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($info: ServerInfoUpdateInput!) {
|
||||
serverInfoUpdate(info: $info)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
info: this.serverModifications
|
||||
}
|
||||
})
|
||||
await this.$apollo.queries['serverInfo'].refetch()
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#admin-settings {
|
||||
.v-card:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,349 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
Stream Administration
|
||||
<span v-if="adminStreams">
|
||||
({{ adminStreams.items.length }} of {{ adminStreams.totalCount }} streams)
|
||||
</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-subtitle>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
class="mx-4 mt-4"
|
||||
:prepend-inner-icon="'mdi-magnify'"
|
||||
:loading="$apollo.loading"
|
||||
label="Search streams"
|
||||
type="text"
|
||||
single-line
|
||||
clearable
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
></v-text-field>
|
||||
<div class="mx-4">
|
||||
<div class="d-flex">
|
||||
<v-select
|
||||
v-model="queryLimit"
|
||||
:items="[10, 25, 50]"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="streams per page"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="streamVisibility"
|
||||
:items="['all', 'public', 'private']"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="visibility"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="orderColumn"
|
||||
:items="['updatedAt', 'size']"
|
||||
rounded
|
||||
dense
|
||||
filled
|
||||
flat
|
||||
label="order streams by"
|
||||
class="mr-2"
|
||||
></v-select>
|
||||
<v-btn
|
||||
fab
|
||||
elevation="0"
|
||||
@click="orderDirection = orderDirection === 'desc' ? 'asc' : 'desc'"
|
||||
>
|
||||
<v-icon>mdi-arrow-expand-{{ orderDirection === 'desc' ? 'down' : 'up' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-subtitle>
|
||||
<v-list v-if="!$apollo.loading" rounded>
|
||||
<v-list-item-group class="ml-6">
|
||||
<v-list-item v-for="stream in adminStreams.items" :key="stream.id" two-line>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<router-link
|
||||
class="text-h6 text-decoration-none"
|
||||
:to="`/streams/${stream.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ stream.description ? stream.description : 'Stream has no description' }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="mt-1">
|
||||
<v-chip small>
|
||||
<v-icon small>
|
||||
{{ stream.isPublic ? 'mdi-lock-open-variant-outline' : 'mdi-lock-outline' }}
|
||||
</v-icon>
|
||||
</v-chip>
|
||||
<v-chip class="mx-2" small>
|
||||
Last activity
|
||||
<timeago :datetime="stream.updatedAt" class="ml-1 mr-"></timeago>
|
||||
</v-chip>
|
||||
<v-chip small class="mr-2 pr-5">
|
||||
<v-icon small class="mr-2">mdi-source-branch</v-icon>
|
||||
{{ stream.branches.totalCount }}
|
||||
</v-chip>
|
||||
<v-chip small class="mr-2">
|
||||
Data usage {{ `${(stream.size ? stream.size / 1048576 : 0.0).toFixed(2)} MB` }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<div
|
||||
style="cursor: pointer; min-height: 33px; line-height: 33px"
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-3' : 'lighten-3'} rounded-xl px-2`"
|
||||
>
|
||||
<user-avatar
|
||||
v-for="user in stream.collaborators.slice(0, 3)"
|
||||
v-show="$vuetify.breakpoint.smAndUp"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:show-hover="false"
|
||||
:size="25"
|
||||
></user-avatar>
|
||||
|
||||
<v-avatar
|
||||
v-if="stream.collaborators.length > 3"
|
||||
v-show="$vuetify.breakpoint.smAndUp"
|
||||
class="ml-1"
|
||||
size="25"
|
||||
color="primary"
|
||||
>
|
||||
<span class="white--text caption">+{{ stream.collaborators.length - 3 }}</span>
|
||||
</v-avatar>
|
||||
<v-avatar v-show="$vuetify.breakpoint.xsOnly" class="ml-1" size="25" color="primary">
|
||||
<span class="white--text caption">{{ stream.collaborators.length }}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</v-list-item-action>
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="initiateDeleteStreams(stream)">
|
||||
<v-icon>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
<div class="text-center">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="numberOfPages"
|
||||
:total-visible="7"
|
||||
circle
|
||||
></v-pagination>
|
||||
</div>
|
||||
</v-list>
|
||||
<v-skeleton-loader v-else class="mx-auto" type="card"></v-skeleton-loader>
|
||||
<v-dialog v-model="showDeleteDialog" persistent max-width="600px">
|
||||
<v-card v-if="showDeleteDialog">
|
||||
<v-toolbar flat class="mb-6">
|
||||
<v-toolbar-title>Confirm stream deletion</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert type="error">
|
||||
Confirm deletion of
|
||||
<b>{{ manipulatedStream.name }}</b>
|
||||
stream from the server.
|
||||
<br />
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="showDeleteDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="deleteStreams">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default {
|
||||
name: 'AdminStreams',
|
||||
components: { UserAvatar },
|
||||
props: {
|
||||
page: { type: [Number, String], required: false, default: null },
|
||||
limit: { type: [Number, String], required: false, default: 10 },
|
||||
q: { type: String, required: false, default: null },
|
||||
orderBy: { type: String, required: false, default: 'updatedAt,desc' },
|
||||
visibility: { type: String, required: false, default: 'all' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteDialog: false,
|
||||
manipulatedStream: null,
|
||||
adminStreams: {
|
||||
items: [],
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
numberOfPages() {
|
||||
return Math.ceil(this.adminStreams.totalCount / this.queryLimit)
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.page ? parseInt(this.page) : 1
|
||||
},
|
||||
set(page) {
|
||||
this.navigateNext({ page })
|
||||
}
|
||||
},
|
||||
queryLimit: {
|
||||
get() {
|
||||
return this.limit ? parseInt(this.limit) : 10
|
||||
},
|
||||
set(limit) {
|
||||
this.navigateNext({ limit })
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
get() {
|
||||
return this.q
|
||||
},
|
||||
set: debounce(function (q) {
|
||||
this.navigateNext({ q })
|
||||
}, 1000)
|
||||
},
|
||||
orderColumn: {
|
||||
get() {
|
||||
let [column] = this.orderBy.split(',')
|
||||
return column
|
||||
},
|
||||
set(column) {
|
||||
let direction = this.orderBy.split(',').pop()
|
||||
let orderBy = `${column},${direction}`
|
||||
this.navigateNext({ orderBy })
|
||||
}
|
||||
},
|
||||
orderDirection: {
|
||||
get() {
|
||||
return this.orderBy.split(',').pop()
|
||||
},
|
||||
set(direction) {
|
||||
let [column] = this.orderBy.split(',')
|
||||
let orderBy = `${column},${direction}`
|
||||
this.navigateNext({ orderBy })
|
||||
}
|
||||
},
|
||||
streamVisibility: {
|
||||
get() {
|
||||
return this.visibility
|
||||
},
|
||||
set(visibility) {
|
||||
this.navigateNext({ visibility })
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initiateDeleteStreams(stream) {
|
||||
this.showDeleteDialog = true
|
||||
this.manipulatedStream = stream
|
||||
},
|
||||
async deleteStreams() {
|
||||
let ids = [this.manipulatedStream.id]
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($ids: [String!]) {
|
||||
streamsDelete(ids: $ids)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
ids: ids
|
||||
},
|
||||
update: () => {
|
||||
this.$apollo.queries.adminStreams.refetch()
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
this.manipulatedStream = null
|
||||
this.showDeleteDialog = false
|
||||
},
|
||||
navigateNext(routeParams) {
|
||||
this.$router.push(this._prepareRoute(routeParams))
|
||||
},
|
||||
_prepareRoute(routeParams) {
|
||||
let newRoute = 'streams'
|
||||
let newParams = { ...this.$props }
|
||||
|
||||
for (let key in routeParams) {
|
||||
newParams[key] = routeParams[key]
|
||||
}
|
||||
|
||||
Object.keys(newParams).forEach((attr, index) => {
|
||||
index === 0 ? (newRoute += '?') : (newRoute += '&')
|
||||
if (newParams[attr]) newRoute += `${attr}=${newParams[attr]}`
|
||||
})
|
||||
return newRoute
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
adminStreams: {
|
||||
query: gql`
|
||||
query Streams(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$orderBy: String
|
||||
$query: String
|
||||
$visibility: String
|
||||
) {
|
||||
adminStreams(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
orderBy: $orderBy
|
||||
query: $query
|
||||
visibility: $visibility
|
||||
) {
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
size
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
offset: (this.currentPage - 1) * this.queryLimit,
|
||||
limit: this.queryLimit,
|
||||
query: this.searchQuery,
|
||||
orderBy: this.orderBy,
|
||||
visibility: this.streamVisibility
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,298 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
Server users
|
||||
<span v-if="users">({{ users.items.length }} of {{ users.totalCount }} users)</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
class="mx-4 mt-4"
|
||||
:prepend-inner-icon="'mdi-magnify'"
|
||||
:loading="$apollo.loading"
|
||||
label="Search users"
|
||||
type="text"
|
||||
single-line
|
||||
clearable
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
></v-text-field>
|
||||
<v-list v-if="!$apollo.loading" rounded>
|
||||
<v-list-item-group v-if="users.totalCount > 0" color="primary">
|
||||
<v-list-item v-for="user in users.items" :key="user.id">
|
||||
<v-list-item-avatar class="d-flex justify-start" :size="50">
|
||||
<user-avatar-icon :avatar="user.avatar" :seed="user.id" :size="50"></user-avatar-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<router-link class="text-h6" :to="`/profile/${user.id}`" target="_blank">
|
||||
{{ user.name }}
|
||||
</router-link>
|
||||
</v-list-item-title>
|
||||
|
||||
<span class="caption">
|
||||
<v-icon x-small>mdi-email-outline</v-icon>
|
||||
{{ user.email }} | {{user.verified}}
|
||||
</span>
|
||||
<span v-if="user.company" class="caption">
|
||||
<v-icon x-small>mdi-domain</v-icon>
|
||||
{{ user.company }}
|
||||
</span>
|
||||
<span v-else class="caption">
|
||||
<v-icon x-small>mdi-help-circle</v-icon>
|
||||
No company info
|
||||
</span>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action class="pt-2">
|
||||
<v-select
|
||||
:value="user.role"
|
||||
:items="availableRoles"
|
||||
label="user role"
|
||||
@change="changeUserRole(user, ...arguments)"
|
||||
></v-select>
|
||||
</v-list-item-action>
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="initiateDeleteUser(user)">
|
||||
<v-icon>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<div class="text-center">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="numberOfPages"
|
||||
:total-visible="7"
|
||||
circle
|
||||
></v-pagination>
|
||||
</div>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-skeleton-loader v-else class="mx-auto" type="card"></v-skeleton-loader>
|
||||
<v-dialog v-model="showConfirmDialog" persistent max-width="600px">
|
||||
<v-card v-if="showConfirmDialog">
|
||||
<v-toolbar flat class="mb-6">
|
||||
<v-toolbar-title>Confirm user role change</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert v-if="newRole === 'server:admin'" type="error">
|
||||
Make sure you trust {{ manipulatedUser.name }}!
|
||||
<br />
|
||||
An admin on the server has access to every resource.
|
||||
</v-alert>
|
||||
You are changing {{ manipulatedUser.name }}'s server access role from
|
||||
{{ roleLookupTable[manipulatedUser.role] }} to {{ roleLookupTable[newRole] }}.
|
||||
<br />
|
||||
Proceed?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="cancelRoleChange">Cancel</v-btn>
|
||||
<v-btn color="primary" text @click="proceedRoleChange">
|
||||
Change role to {{ roleLookupTable[newRole] }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showDeleteDialog" persistent max-width="600px">
|
||||
<v-card v-if="showDeleteDialog">
|
||||
<v-toolbar flat class="mb-6">
|
||||
<v-toolbar-title>Confirm user deletion</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert type="error">
|
||||
Confirm deletion of
|
||||
<b>{{ manipulatedUser.name }}</b>
|
||||
's account from the server.
|
||||
<br />
|
||||
Streams, where {{ manipulatedUser.name }} is the only owner, will also be deleted.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="showDeleteDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="deleteUser(manipulatedUser)">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default {
|
||||
name: 'UserAdmin',
|
||||
components: { UserAvatarIcon },
|
||||
props: {
|
||||
limit: { type: [Number, String], required: false, default: 10 },
|
||||
page: { type: [Number, String], required: false, default: 1 },
|
||||
q: { type: String, required: false, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roleLookupTable: {
|
||||
'server:user': 'User',
|
||||
'server:admin': 'Admin',
|
||||
'server:archived-user': 'Archived'
|
||||
},
|
||||
users: {
|
||||
items: [],
|
||||
totalCount: 0
|
||||
},
|
||||
// currentPage: 1,
|
||||
// searchQuery: null,
|
||||
showConfirmDialog: false,
|
||||
showDeleteDialog: false,
|
||||
manipulatedUser: null,
|
||||
newRole: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
queryLimit() {
|
||||
return parseInt(this.limit)
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return parseInt(this.page)
|
||||
},
|
||||
set(newPage) {
|
||||
this.paginateNext(newPage)
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
get() {
|
||||
return this.q
|
||||
},
|
||||
set: debounce(function (q) {
|
||||
this.applySearch(q)
|
||||
}, 500)
|
||||
},
|
||||
queryOffset() {
|
||||
return (this.page - 1) * this.queryLimit
|
||||
},
|
||||
numberOfPages() {
|
||||
return Math.ceil(this.users.totalCount / this.limit)
|
||||
},
|
||||
availableRoles() {
|
||||
let roleItems = []
|
||||
for (let role in this.roleLookupTable) {
|
||||
roleItems.push({ text: this.roleLookupTable[role], value: role })
|
||||
}
|
||||
return roleItems
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initiateDeleteUser(user) {
|
||||
this.showDeleteDialog = true
|
||||
this.manipulatedUser = user
|
||||
},
|
||||
async deleteUser(user) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($userEmail: String!) {
|
||||
adminDeleteUser(userConfirmation: { email: $userEmail })
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
userEmail: user.email
|
||||
},
|
||||
update: () => {
|
||||
this.$apollo.queries.users.refetch()
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
this.resetManipulatedUser()
|
||||
this.showDeleteDialog = false
|
||||
},
|
||||
changeUserRole(user, args) {
|
||||
this.manipulatedUser = user
|
||||
this.newRole = args
|
||||
|
||||
this.showConfirmDialog = true
|
||||
},
|
||||
resetManipulatedUser() {
|
||||
this.manipulatedUser = null
|
||||
this.newRole = null
|
||||
},
|
||||
cancelRoleChange() {
|
||||
this.showConfirmDialog = false
|
||||
this.$apollo.queries.users.refetch()
|
||||
this.resetManipulatedUser()
|
||||
},
|
||||
async proceedRoleChange() {
|
||||
await this.updateUserRole(this.manipulatedUser.id, this.newRole)
|
||||
this.resetManipulatedUser()
|
||||
this.showConfirmDialog = false
|
||||
},
|
||||
async updateUserRole(userId, newRole) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($userId: String!, $newRole: String!) {
|
||||
userRoleChange(userRoleInput: { id: $userId, role: $newRole })
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
userId,
|
||||
newRole
|
||||
},
|
||||
update: () => {
|
||||
this.$apollo.queries.users.refetch()
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
},
|
||||
paginateNext(newPage) {
|
||||
this.$router.push(this._prepareRoute(newPage, this.limit, this.searchQuery))
|
||||
},
|
||||
applySearch(searchQuery) {
|
||||
this.$router.push(this._prepareRoute(1, this.limit, searchQuery))
|
||||
},
|
||||
_prepareRoute(page, limit, query) {
|
||||
let newRoute = `users?page=${page}&limit=${limit}`
|
||||
if (query) newRoute = `${newRoute}&q=${query}`
|
||||
return newRoute
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
query: gql`
|
||||
query Users($limit: Int, $offset: Int, $query: String) {
|
||||
users(limit: $limit, offset: $offset, query: $query) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
suuid
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
authorizedApps {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
limit: this.queryLimit,
|
||||
offset: this.queryOffset,
|
||||
query: this.q
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,379 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<portal to="streamTitleBar">
|
||||
<div v-if="stream && stream.branch">
|
||||
<v-icon small class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="space-grotesk" style="max-width: 80%">{{ stream.branch.name }}</span>
|
||||
<span class="caption ml-2 mb-2 pb-2">{{ stream.branch.description }}</span>
|
||||
<v-chip
|
||||
v-tooltip="`Branch ${stream.branch.name} has ${stream.branch.commits.totalCount} commits`"
|
||||
class="ml-2 pl-2"
|
||||
small
|
||||
>
|
||||
<v-icon small>mdi-source-commit</v-icon>
|
||||
{{ stream.branch.commits.totalCount }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</portal>
|
||||
<portal to="streamActionsBar">
|
||||
<v-btn
|
||||
v-if="
|
||||
loggedInUserId &&
|
||||
stream &&
|
||||
stream.role !== 'stream:reviewer' &&
|
||||
stream.branch &&
|
||||
stream.branch.name !== 'main'
|
||||
"
|
||||
v-tooltip="'Edit branch'"
|
||||
elevation="0"
|
||||
color="primary"
|
||||
small
|
||||
rounded
|
||||
:fab="$vuetify.breakpoint.mdAndDown"
|
||||
dark
|
||||
@click="editBranch()"
|
||||
>
|
||||
<v-icon small :class="`${$vuetify.breakpoint.mdAndDown ? '' : 'mr-2'}`">mdi-pencil</v-icon>
|
||||
<span class="hidden-md-and-down">Edit</span>
|
||||
</v-btn>
|
||||
</portal>
|
||||
<v-row no-gutters>
|
||||
<v-col v-if="stream && stream.branch" cols="12" class="pa-4">
|
||||
<v-row v-if="stream.branch.commits.items.length > 0">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<router-link :to="`/streams/${streamId}/commits/${latestCommit.id}`">
|
||||
<preview-image
|
||||
:height="320"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${latestCommit.id}`"
|
||||
></preview-image>
|
||||
</router-link>
|
||||
<div style="position: absolute; top: 10px; right: 20px">
|
||||
<commit-received-receipts :stream-id="streamId" :commit-id="latestCommit.id" />
|
||||
</div>
|
||||
<div style="position: absolute; top: 10px; left: 12px">
|
||||
<source-app-avatar :application-name="latestCommit.sourceApplication" />
|
||||
</div>
|
||||
<v-list-item class="elevation-0">
|
||||
<v-list-item-icon class="">
|
||||
<user-avatar
|
||||
:id="latestCommit.authorId"
|
||||
:avatar="latestCommit.authorAvatar"
|
||||
:name="latestCommit.authorName"
|
||||
:size="40"
|
||||
/>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<router-link
|
||||
class="text-decoration-none"
|
||||
:to="`/streams/${streamId}/commits/${latestCommit.id}`"
|
||||
>
|
||||
<v-list-item-title class="mt-0 pt-0 py-1">
|
||||
{{ latestCommit.message }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ latestCommit.authorName }}</b>
|
||||
|
||||
<timeago :datetime="latestCommit.createdAt"></timeago>
|
||||
<!-- ({{ commitDate }}) -->
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>Older Commits</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-tooltip="`View as a ${listMode ? 'gallery' : 'list'}`"
|
||||
icon
|
||||
@click="listMode = !listMode"
|
||||
>
|
||||
<v-icon v-if="listMode">mdi-view-dashboard</v-icon>
|
||||
<v-icon v-else>mdi-view-list</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!listMode">
|
||||
<v-col
|
||||
v-for="commit in allPreviousCommits"
|
||||
:key="commit.id + 'card'"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
xl="3"
|
||||
>
|
||||
<v-card>
|
||||
<router-link :to="`/streams/${streamId}/commits/${commit.id}`">
|
||||
<preview-image
|
||||
:height="180"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${commit.id}`"
|
||||
></preview-image>
|
||||
</router-link>
|
||||
<div style="position: absolute; top: 10px; right: 20px">
|
||||
<commit-received-receipts :stream-id="streamId" :commit-id="commit.id" />
|
||||
</div>
|
||||
<div style="position: absolute; top: 10px; left: 12px">
|
||||
<source-app-avatar :application-name="commit.sourceApplication" />
|
||||
</div>
|
||||
<v-list-item class="elevation-0">
|
||||
<v-list-item-icon class="">
|
||||
<user-avatar
|
||||
:id="commit.authorId"
|
||||
:avatar="commit.authorAvatar"
|
||||
:name="commit.authorName"
|
||||
:size="40"
|
||||
/>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<router-link
|
||||
class="text-decoration-none"
|
||||
:to="`/streams/${streamId}/commits/${commit.id}`"
|
||||
>
|
||||
<v-list-item-title class="mt-0 pt-0 py-1">
|
||||
{{ commit.message }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ commit.authorName }}</b>
|
||||
|
||||
<timeago :datetime="commit.createdAt"></timeago>
|
||||
<!-- ({{ commitDate }}) -->
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="stream && stream.branch && listMode" cols="12" class="pa-0 ma-0">
|
||||
<v-list v-if="stream.branch.commits.items.length > 0" class="pa-0 ma-0">
|
||||
<list-item-commit
|
||||
v-for="item in allPreviousCommits"
|
||||
:key="item.id"
|
||||
:commit="item"
|
||||
:stream-id="streamId"
|
||||
show-received-receipts
|
||||
></list-item-commit>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<infinite-loading
|
||||
v-if="stream && stream.branch.commits.totalCount !== 0"
|
||||
spinner="waveDots"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">
|
||||
<v-col>
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>
|
||||
You've reached the end - this branch has no more commits.
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</div>
|
||||
<div slot="no-results">
|
||||
<v-col>
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>
|
||||
You've reached the end - this branch has no more commits.
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</v-col>
|
||||
</div>
|
||||
</infinite-loading>
|
||||
|
||||
<!-- <v-col v-if="$apollo.loading">
|
||||
<v-skeleton-loader type="article, article"></v-skeleton-loader>
|
||||
</v-col> -->
|
||||
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<no-data-placeholder
|
||||
v-if="!$apollo.loading && stream.branch && stream.branch.commits.totalCount === 0"
|
||||
>
|
||||
<h2 class="space-grotesk">Branch "{{ stream.branch.name }}" has no commits.</h2>
|
||||
</no-data-placeholder>
|
||||
</v-row>
|
||||
<error-placeholder
|
||||
v-if="!$apollo.loading && (error || stream.branch === null)"
|
||||
error-type="404"
|
||||
>
|
||||
<h2>{{ error || `Branch "${$route.params.branchName}" does not exist.` }}</h2>
|
||||
</error-placeholder>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import branchQuery from '@/graphql/branch.gql'
|
||||
|
||||
export default {
|
||||
name: 'Branch',
|
||||
components: {
|
||||
InfiniteLoading: () => import('vue-infinite-loading'),
|
||||
ListItemCommit: () => import('@/components/ListItemCommit'),
|
||||
BranchEditDialog: () => import('@/components/dialogs/BranchEditDialog'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
|
||||
PreviewImage: () => import('@/components/PreviewImage'),
|
||||
CommitReceivedReceipts: () => import('@/components/CommitReceivedReceipts'),
|
||||
UserAvatar: () => import('@/components/UserAvatar'),
|
||||
SourceAppAvatar: () => import('@/components/SourceAppAvatar'),
|
||||
Renderer: () => import('@/components/Renderer')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogEdit: false,
|
||||
error: null,
|
||||
listMode: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: branchQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
branchName: this.$route.params.branchName.toLowerCase()
|
||||
}
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
error(err) {
|
||||
console.log(err)
|
||||
}
|
||||
},
|
||||
commitDeleted: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitDeleted(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
error(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loggedInUserId() {
|
||||
return localStorage.getItem('uuid')
|
||||
},
|
||||
streamId() {
|
||||
return this.$route.params.streamId
|
||||
},
|
||||
latestCommitObjectUrl() {
|
||||
if (
|
||||
this.stream &&
|
||||
this.stream.branch &&
|
||||
this.stream.branch.commits.items &&
|
||||
this.stream.branch.commits.items.length > 0
|
||||
)
|
||||
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.branch.commits.items[0].referencedObject}`
|
||||
else return null
|
||||
},
|
||||
latestCommit() {
|
||||
if (this.stream.branch.commits.items && this.stream.branch.commits.items.length > 0)
|
||||
return this.stream.branch.commits.items[0]
|
||||
else return null
|
||||
},
|
||||
allPreviousCommits() {
|
||||
if (this.stream.branch.commits.items && this.stream.branch.commits.items.length > 0)
|
||||
return this.stream.branch.commits.items.slice(1)
|
||||
else return null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.params.branchName === 'globals')
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
|
||||
},
|
||||
methods: {
|
||||
editBranch() {
|
||||
this.$refs.editBranchDialog.open(this.stream.branch).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
else if (dialog.deleted) {
|
||||
this.$emit('refetch-branches')
|
||||
this.$router.push({ path: `/streams/${this.streamId}` })
|
||||
} else if (dialog.name !== this.$route.params.branchName) {
|
||||
//this.$router.push does not work, refresh entire window
|
||||
|
||||
this.$router.push({
|
||||
path: `/streams/${this.streamId}/branches/${encodeURIComponent(dialog.name)}`
|
||||
})
|
||||
} else {
|
||||
this.$emit('refetch-branches')
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
})
|
||||
},
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.stream.fetchMore({
|
||||
variables: {
|
||||
cursor: this.stream.branch.commits.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.stream.branch.commits.items
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
stream: {
|
||||
__typename: previousResult.stream.__typename,
|
||||
name: previousResult.stream.name,
|
||||
id: previousResult.stream.id,
|
||||
branch: {
|
||||
id: fetchMoreResult.stream.branch.id,
|
||||
name: fetchMoreResult.stream.branch.name,
|
||||
description: fetchMoreResult.stream.branch.description,
|
||||
__typename: previousResult.stream.branch.__typename,
|
||||
commits: {
|
||||
__typename: previousResult.stream.branch.commits.__typename,
|
||||
cursor: fetchMoreResult.stream.branch.commits.cursor,
|
||||
totalCount: fetchMoreResult.stream.branch.commits.totalCount,
|
||||
items: [...previousResult.stream.branch.commits.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-item-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
@@ -1,375 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-account-multiple</v-icon>
|
||||
<span class="space-grotesk">Stream Collaborators</span>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<v-alert v-if="stream.role !== 'stream:owner'" type="warning">
|
||||
Your permission level ({{ stream.role }}) is not high enough to edit this stream's
|
||||
collaborators.
|
||||
</v-alert>
|
||||
<v-card v-if="serverInfo" elevation="0" color="transparent" :class="`mb-4 py-4`">
|
||||
<v-row align="stretch">
|
||||
<v-col v-for="role in roles" :key="role.name" cols="12" sm="4">
|
||||
<v-card
|
||||
rounded="lg"
|
||||
style="height: 100%"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} d-flex flex-column`"
|
||||
>
|
||||
<v-toolbar style="flex: none" flat>
|
||||
<v-toolbar-title class="text-capitalize">
|
||||
{{ role.name.split(':')[1] }}s
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-badge
|
||||
inline
|
||||
:content="getRoleCount(role.name)"
|
||||
:color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"
|
||||
></v-badge>
|
||||
</v-toolbar>
|
||||
<v-card-text class="flex-grow-1">{{ role.description }}</v-card-text>
|
||||
<v-card-text class="mt-auto">
|
||||
<div v-if="role.name === 'stream:reviewer'" class="align-self-end">
|
||||
<user-avatar
|
||||
v-for="user in reviewers"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
<span v-if="reviewers.length === 0">No users with this role.</span>
|
||||
</div>
|
||||
<div v-if="role.name === 'stream:contributor'">
|
||||
<user-avatar
|
||||
v-for="user in contributors"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
<span v-if="contributors.length === 0">No users with this role.</span>
|
||||
</div>
|
||||
<div v-if="role.name === 'stream:owner'">
|
||||
<user-avatar
|
||||
v-for="user in owners"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card
|
||||
v-if="stream"
|
||||
:loading="loading"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
|
||||
<v-toolbar
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
flat
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<v-icon small class="mr-2">mdi-account-plus</v-icon>
|
||||
<span class="d-inline-block">Add collaborators</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text v-if="stream.role === 'stream:owner'">
|
||||
<p>
|
||||
Default role for new collaborators is that of a stream contributor. You will be able to
|
||||
change it after they're added.
|
||||
</p>
|
||||
<v-text-field v-model="search" label="Search for a user" persistent-hint />
|
||||
<div v-if="$apollo.loading">Searching.</div>
|
||||
<v-list
|
||||
v-if="search && search.length >= 3 && userSearch && userSearch.items"
|
||||
dense
|
||||
one-line
|
||||
class="px-0 mx-0 transparent"
|
||||
>
|
||||
<v-list-item v-if="filteredSearchResults.length === 0" class="px-0 mx-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
No users found. Note: you can search by name and email.
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="filteredSearchResults.length === 0" class="px-0 mx-0">
|
||||
<v-list-item-action>
|
||||
<v-btn color="primary" @click="showStreamInviteDialog">Invite {{ search }}</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="item in filteredSearchResults"
|
||||
v-else
|
||||
:key="item.id"
|
||||
@click="addCollab(item)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<user-avatar
|
||||
:id="item.id"
|
||||
:name="item.name"
|
||||
:avatar="item.avatar"
|
||||
:size="25"
|
||||
class="ml-1"
|
||||
></user-avatar>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.company ? item.company : 'no company info' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<stream-invite-dialog ref="streamInviteDialog" :stream-id="stream.id" :text="search" />
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
v-if="stream"
|
||||
:loading="loading"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:class="`mt-5 ${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-toolbar
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
flat
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<v-icon small class="mr-2">mdi-account-group</v-icon>
|
||||
<span class="d-inline-block">Edit roles</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text class="px-0">
|
||||
<p v-if="collaborators.length === 0" class="ml-4">
|
||||
There are no collaborators on this stream. Speckle is more fun in multiplayer mode, so
|
||||
invite someone!
|
||||
</p>
|
||||
<v-list v-else class="transparent">
|
||||
<v-list-item v-for="user in collaborators" :key="user.id" two-lines>
|
||||
<v-list-item-icon>
|
||||
<user-avatar :id="user.id" :avatar="user.avatar" :name="user.name" :size="42" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">{{ user.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-select
|
||||
v-model="user.role"
|
||||
item-value="name"
|
||||
:items="roles"
|
||||
class="py-0 my-0"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@change="setUserPermissions(user)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="pa-2">
|
||||
<p class="pa-0 ma-0">{{ item.name }}</p>
|
||||
<p class="caption pa-0 ma-0 grey--text" style="max-width: 300px">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
icon
|
||||
small
|
||||
color="error"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@click="removeUser(user)"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import serverQuery from '@/graphql/server.gql'
|
||||
import streamCollaboratorsQuery from '@/graphql/streamCollaborators.gql'
|
||||
import userSearchQuery from '@/graphql/userSearch.gql'
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import StreamInviteDialog from '@/components/dialogs/StreamInviteDialog'
|
||||
|
||||
export default {
|
||||
components: { UserAvatar, StreamInviteDialog },
|
||||
data: () => ({
|
||||
search: '',
|
||||
selectedUsers: null,
|
||||
selectedRole: null,
|
||||
userSearch: { items: [] },
|
||||
serverInfo: { roles: [] },
|
||||
loading: false
|
||||
}),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamCollaboratorsQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
},
|
||||
userSearch: {
|
||||
query: userSearchQuery,
|
||||
variables() {
|
||||
return {
|
||||
query: this.search,
|
||||
limit: 25
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
},
|
||||
serverInfo: {
|
||||
prefetch: true,
|
||||
query: serverQuery
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
roles() {
|
||||
return this.serverInfo.roles.filter((x) => x.resourceTarget === 'streams')
|
||||
},
|
||||
collaborators() {
|
||||
if (!this.stream) return []
|
||||
return this.stream.collaborators.filter((user) => user.id !== this.myId)
|
||||
},
|
||||
reviewers() {
|
||||
if (!this.stream) return []
|
||||
return this.stream.collaborators.filter((u) => u.role === 'stream:reviewer')
|
||||
},
|
||||
contributors() {
|
||||
if (!this.stream) return []
|
||||
return this.stream.collaborators.filter((u) => u.role === 'stream:contributor')
|
||||
},
|
||||
owners() {
|
||||
if (!this.stream) return []
|
||||
return this.stream.collaborators.filter((u) => u.role === 'stream:owner')
|
||||
},
|
||||
filteredSearchResults() {
|
||||
if (!this.userSearch) return null
|
||||
let users = []
|
||||
for (let u of this.userSearch.items) {
|
||||
if (u.id === this.myId) continue
|
||||
let indx = this.collaborators.findIndex((eu) => eu.id === u.id)
|
||||
if (indx === -1) users.push(u)
|
||||
}
|
||||
return users
|
||||
},
|
||||
myId() {
|
||||
return localStorage.getItem('uuid')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRoleCount(role) {
|
||||
if (role === 'stream:owner') return this.owners.length || '0'
|
||||
if (role === 'stream:contributor') return this.contributors.length || '0'
|
||||
if (role === 'stream:reviewer') return this.reviewers.length || '0'
|
||||
},
|
||||
async removeUser(user) {
|
||||
this.loading = true
|
||||
this.$matomo && this.$matomo.trackPageView('stream/remove-collaborator')
|
||||
this.$mixpanel.track('Permission Action', { type: 'action', name: 'remove', hostApp: 'web' })
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation streamRevokePermission($params: StreamRevokePermissionInput!) {
|
||||
streamRevokePermission(permissionParams: $params)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.stream.id,
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
let index = this.stream.collaborators.findIndex((u) => u.id === user.id)
|
||||
if (index !== -1) {
|
||||
this.stream.collaborators.splice(index, 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
this.$apollo.queries.stream.refetch()
|
||||
this.loading = false
|
||||
},
|
||||
async setUserPermissions(user) {
|
||||
this.loading = true
|
||||
await this.grantPermissionUser(user)
|
||||
this.loading = false
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
async addCollab(user) {
|
||||
this.loading = true
|
||||
this.search = null
|
||||
this.userSearch.items = null
|
||||
user.role = 'stream:contributor'
|
||||
await this.grantPermissionUser(user)
|
||||
this.stream.collaborators.unshift(user)
|
||||
this.loading = false
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
async grantPermissionUser(user) {
|
||||
this.$matomo && this.$matomo.trackPageView('stream/add-collaborator')
|
||||
this.$mixpanel.track('Permission Action', { type: 'action', name: 'add', hostApp: 'web' })
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation grantPerm($params: StreamGrantPermissionInput!) {
|
||||
streamGrantPermission(permissionParams: $params)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.stream.id,
|
||||
userId: user.id,
|
||||
role: user.role
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
showStreamInviteDialog() {
|
||||
this.$refs.streamInviteDialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,263 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row v-if="$apollo.queries.stream.loading" no-gutters>
|
||||
<v-col cols="12" class="ma-0 pa-0">
|
||||
<v-card>
|
||||
<v-skeleton-loader type="list-item-avatar, card-avatar, article"></v-skeleton-loader>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="stream">
|
||||
<portal to="streamTitleBar">
|
||||
<commit-toolbar :stream="stream" @edit-commit="showCommitEditDialog = true" />
|
||||
</portal>
|
||||
</v-row>
|
||||
<div style="height: 100vh; width: 100%; top: -64px; position: absolute">
|
||||
<renderer :object-url="commitObjectUrl" @selection="handleSelection" />
|
||||
</div>
|
||||
|
||||
<div v-if="stream" cols="12" class="ma-0 pa-0" style="position: relative; top: -64px">
|
||||
<portal to="nav">
|
||||
<v-list v-if="stream" style="padding-left: 10px" nav dense class="mt-0 pt-0" expand>
|
||||
<v-list-item
|
||||
link
|
||||
:to="`/streams/${stream.id}/branches/${stream.commit.branchName}`"
|
||||
class=""
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
<v-icon small class="mr-1 caption">mdi-source-branch</v-icon>
|
||||
{{ stream.commit.branchName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-new</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
TODO: Insert commit menu; mostly viewer based
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-card v-if="false" rounded="lg" style="width: 100%" class="transparent elevation-0">
|
||||
<!-- Selected object -->
|
||||
<v-expand-transition>
|
||||
<v-sheet v-show="selectionData.length !== 0" class="pa-4" color="transparent">
|
||||
<v-card-title class="mr-8">
|
||||
<v-badge inline :content="selectionData.length">
|
||||
<v-icon class="mr-2">mdi-cube</v-icon>
|
||||
Selection
|
||||
</v-badge>
|
||||
</v-card-title>
|
||||
<div v-if="selectionData.length !== 0">
|
||||
<object-simple-viewer
|
||||
v-for="(obj, ind) in selectionData"
|
||||
:key="obj.id + ind"
|
||||
:value="obj"
|
||||
:stream-id="stream.id"
|
||||
:key-name="`Selected Object ${ind + 1}`"
|
||||
force-show-open-in-new
|
||||
force-expand
|
||||
/>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-expand-transition>
|
||||
<!-- Object explorer -->
|
||||
<v-card class="pa-4" rounded="lg" color="transparent">
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-database</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Data</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<commit-received-receipts
|
||||
:stream-id="$route.params.streamId"
|
||||
:commit-id="stream.commit.id"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<object-speckle-viewer
|
||||
class="mt-4"
|
||||
:stream-id="stream.id"
|
||||
:object-id="stream.commit.referencedObject"
|
||||
:value="commitObject"
|
||||
:expand="true"
|
||||
></object-speckle-viewer>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card>
|
||||
</portal>
|
||||
</div>
|
||||
|
||||
<v-row v-if="!$apollo.queries.stream.loading && !stream.commit" justify="center">
|
||||
<error-placeholder error-type="404">
|
||||
<h2>Commit {{ $route.params.commitId }} not found.</h2>
|
||||
</error-placeholder>
|
||||
</v-row>
|
||||
<commit-edit-dialog
|
||||
ref="commitDialog"
|
||||
@show-delete="showDeleteDialog = true"
|
||||
></commit-edit-dialog>
|
||||
|
||||
<v-dialog v-model="showDeleteDialog" width="500">
|
||||
<v-card class="pa-0 transparent">
|
||||
<v-alert type="info" class="ma-0">
|
||||
<h3>Are you sure?</h3>
|
||||
You cannot undo this action. This will permanently delete the commit
|
||||
<v-chip
|
||||
:to="`/streams/${$route.params.streamId}/commits/${
|
||||
stream && stream.commit ? stream.commit.id : null
|
||||
}`"
|
||||
color="primary"
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left" light>mdi-timeline-remove-outline</v-icon>
|
||||
{{ stream && stream.commit ? stream.commit.id : null }}
|
||||
</v-chip>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
<v-btn text class="error--text" @click="deleteCommit">Delete</v-btn>
|
||||
<v-btn text @click="showDeleteDialog = false">Cancel</v-btn>
|
||||
</v-alert>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import streamCommitQuery from '@/graphql/commit.gql'
|
||||
|
||||
export default {
|
||||
name: 'Branch',
|
||||
components: {
|
||||
CommitEditDialog: () => import('@/components/dialogs/CommitEditDialog'),
|
||||
ObjectSpeckleViewer: () => import('@/components/ObjectSpeckleViewer'),
|
||||
ObjectSimpleViewer: () => import('@/components/ObjectSimpleViewer'),
|
||||
Renderer: () => import('@/components/Renderer'),
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
|
||||
CommitReceivedReceipts: () => import('@/components/CommitReceivedReceipts'),
|
||||
CommitToolbar: () => import('@/cleanup/toolbars/CommitToolbar')
|
||||
},
|
||||
data: () => ({
|
||||
loadedModel: false,
|
||||
selectionData: [],
|
||||
showCommitEditDialg: false,
|
||||
showDeleteDialog: false
|
||||
}),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamCommitQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
id: this.$route.params.commitId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loggedInUserId() {
|
||||
return localStorage.getItem('uuid')
|
||||
},
|
||||
commitDate() {
|
||||
if (!this.stream.commit) return null
|
||||
let date = new Date(this.stream.commit.createdAt)
|
||||
let options = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
|
||||
return date.toLocaleString(undefined, options)
|
||||
},
|
||||
commitObject() {
|
||||
return {
|
||||
speckle_type: 'reference',
|
||||
referencedId: this.stream?.commit.referencedObject
|
||||
}
|
||||
},
|
||||
commitObjectUrl() {
|
||||
return `${window.location.origin}/streams/${this.stream?.id}/objects/${this.commitObject.referencedId}`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stream(val) {
|
||||
if (!val) return
|
||||
if (val && val.commit && val.commit.branchName && val.commit.branchName === 'globals')
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/globals/${val.commit.id}`)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelection(selectionData) {
|
||||
this.selectionData.splice(0, this.selectionData.length)
|
||||
this.selectionData.push(...selectionData)
|
||||
},
|
||||
editCommit() {
|
||||
this.$refs.commitDialog.open(this.stream.commit, this.stream.id).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
|
||||
this.$matomo && this.$matomo.trackPageView('commit/update')
|
||||
this.$mixpanel.track('Commit Action', { type: 'action', name: 'update', hostApp: 'web' })
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation commitUpdate($myCommit: CommitUpdateInput!) {
|
||||
commitUpdate(commit: $myCommit)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myCommit: { ...dialog.commit }
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteCommit() {
|
||||
this.$matomo && this.$matomo.trackPageView('commit/delete')
|
||||
this.$mixpanel.track('Commit Action', { type: 'action', name: 'delete', hostApp: 'web' })
|
||||
let commitBranch = null
|
||||
if (
|
||||
this.stream &&
|
||||
this.stream.commit &&
|
||||
this.stream.commit.branchName &&
|
||||
this.stream.commit.branchName
|
||||
)
|
||||
commitBranch = this.stream.commit.branchName
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation commitUpdate($myCommit: CommitDeleteInput!) {
|
||||
commitDelete(commit: $myCommit)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myCommit: {
|
||||
streamId: this.stream.id,
|
||||
id: this.stream.commit.id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
this.showDeleteDialog = false
|
||||
//window.location.href = window.origin + `/streams/` + this.$route.params.streamId + `/branches/` + commitBranch //go to branch page, refresh all
|
||||
this.$router.push(`/streams/` + this.$route.params.streamId + `/branches/` + commitBranch)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row v-if="stream && stream.commits.totalCount !== 0" class="pa-3">
|
||||
<v-col cols="12" class="pa-4">
|
||||
<v-card :to="`/streams/${$route.params.streamId}/commits/${stream.commits.items[0].id}`">
|
||||
<preview-image
|
||||
:height="320"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${stream.commits.items[0].id}`"
|
||||
></preview-image>
|
||||
<list-item-commit
|
||||
:commit="stream.commits.items[0]"
|
||||
:stream-id="$route.params.streamId"
|
||||
class="elevation-0"
|
||||
></list-item-commit>
|
||||
</v-card>
|
||||
<v-list class="pa-0 ma-0"></v-list>
|
||||
</v-col>
|
||||
<v-col cols="12" class="" style="height: 20px"></v-col>
|
||||
|
||||
<v-col
|
||||
cols="12"
|
||||
:xl="loggedIn ? 4 : 12"
|
||||
class="pa-0 ma-0"
|
||||
:order="`${$vuetify.breakpoint.xlOnly ? 'last' : ''}`"
|
||||
>
|
||||
<v-card class="transparent elevation-0">
|
||||
<v-toolbar class="transparent elevation-0">
|
||||
<v-toolbar-title>Latest Active Branches</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<!-- <v-app-bar-nav-icon>
|
||||
<v-icon>mdi-plus-circle</v-icon>
|
||||
</v-app-bar-nav-icon> -->
|
||||
</v-toolbar>
|
||||
<v-card-title class="caption" style="margin-top: -30px">
|
||||
The stream's last three updated branches
|
||||
</v-card-title>
|
||||
<v-row class="pa-4 mt-0">
|
||||
<v-col
|
||||
v-for="branch in latestBranches"
|
||||
:key="branch.name"
|
||||
cols="12"
|
||||
sm="4"
|
||||
md="4"
|
||||
:xl="loggedIn ? 12 : 4"
|
||||
>
|
||||
<v-card :to="`/streams/${$route.params.streamId}/branches/${branch.name}`">
|
||||
<preview-image
|
||||
:height="120"
|
||||
:url="`/preview/${$route.params.streamId}/commits/${branch.commits.items[0].id}`"
|
||||
></preview-image>
|
||||
<v-toolbar flat class="transparent">
|
||||
<v-toolbar-title>
|
||||
<v-icon>mdi-source-branch</v-icon>
|
||||
{{ branch.name }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-badge
|
||||
inline
|
||||
:content="branch.commits.totalCount"
|
||||
:color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"
|
||||
></v-badge>
|
||||
</v-toolbar>
|
||||
<list-item-commit
|
||||
:commit="branch.commits.items[0]"
|
||||
:stream-id="$route.params.streamId"
|
||||
></list-item-commit>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="loggedIn" cols="12" xl="8" class="pr-4">
|
||||
<v-card class="transparent elevation-0">
|
||||
<v-toolbar class="transparent elevation-0">
|
||||
<v-toolbar-title>Stream Feed</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-card-title class="caption" style="margin-top: -30px">Recent activity log</v-card-title>
|
||||
</v-card>
|
||||
<div style="margin-top: -42px">
|
||||
<stream-activity></stream-activity>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<no-data-placeholder v-if="stream && stream.commits.totalCount === 0">
|
||||
<h2>This stream has not received any data.</h2>
|
||||
<p class="caption">
|
||||
Streams are repositories where you can store, version and retrieve various design data.
|
||||
</p>
|
||||
</no-data-placeholder>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'Details',
|
||||
components: {
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
|
||||
ListItemCommit: () => import('@/components/ListItemCommit'),
|
||||
PreviewImage: () => import('@/components/PreviewImage'),
|
||||
StreamActivity: () => import('@/views/stream/Activity')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clearRendererTrigger: 0,
|
||||
error: '',
|
||||
selectedBranch: null
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
isPublic
|
||||
name
|
||||
branches {
|
||||
totalCount
|
||||
items {
|
||||
name
|
||||
description
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
createdAt
|
||||
message
|
||||
referencedObject
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
authorName
|
||||
authorId
|
||||
authorAvatar
|
||||
sourceApplication
|
||||
message
|
||||
referencedObject
|
||||
createdAt
|
||||
branchName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
if (err.message) this.error = err.message.replace('GraphQL error: ', '')
|
||||
else this.error = err
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
streamUpdated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
streamUpdated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
branchCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
branchDeleted: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
branchDeleted(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
latestCommitObjectUrl() {
|
||||
if (this.stream && this.stream.commits.items.length > 0)
|
||||
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.commits.items[0].referencedObject}`
|
||||
else return null
|
||||
},
|
||||
latestBranches() {
|
||||
if (!this.stream) return []
|
||||
|
||||
let branches = this.stream.branches.items
|
||||
.filter((br) => br.name !== 'globals' && br.commits.totalCount !== 0)
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) => new Date(b.commits.items[0].createdAt) - new Date(a.commits.items[0].createdAt)
|
||||
)
|
||||
|
||||
return branches.slice(0, 3)
|
||||
},
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,275 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<portal to="toolbar">
|
||||
<div class="d-flex align-center">
|
||||
<div class="text-truncate">
|
||||
<router-link
|
||||
v-tooltip="stream.name"
|
||||
class="text-decoration-none space-grotesk mx-1"
|
||||
:to="`/streams/${stream.id}`"
|
||||
>
|
||||
<v-icon small class="primary--text mb-1 mr-1">mdi-folder</v-icon>
|
||||
<b>{{ stream.name }}</b>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="text-truncate flex-shrink-0">
|
||||
/
|
||||
<v-icon small class="mx-1 mb-1 hidden-xs-only">mdi-earth</v-icon>
|
||||
<span class="space-grotesk" style="max-width: 80%">Globals Variables</span>
|
||||
</div>
|
||||
</div>
|
||||
</portal>
|
||||
<no-data-placeholder
|
||||
v-if="!objectId && !$apollo.loading && !revealBuilder"
|
||||
:show-message="false"
|
||||
>
|
||||
<h2>There are no global variables in this stream.</h2>
|
||||
<p class="caption">
|
||||
Global variables can hold various information that's useful across the project: location
|
||||
(city, adress, lat & long coordinates), custom project names or tags, or any other numbers
|
||||
or text that you want to keep track of.
|
||||
</p>
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item
|
||||
v-if="stream.role !== 'stream:reviewer'"
|
||||
link
|
||||
class="primary mb-4"
|
||||
dark
|
||||
@click="createGlobals()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Create Globals</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-else class="warning" dark>
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-lock</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content class="caption">
|
||||
You do not have enough permissions to create globals.
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
|
||||
href="https://speckle.guide/user/web.html#globals"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-book-open-variant</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Globals docs</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Read the documentation on global variables.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</no-data-placeholder>
|
||||
|
||||
<div v-if="objectId || revealBuilder">
|
||||
<v-row>
|
||||
<!-- Help -->
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
v-if="true"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-earth</v-icon>
|
||||
<span class="d-inline-block">What are Globals?</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
<p class="caption">
|
||||
Globals are useful for storing design values, project requirements, notes, or any
|
||||
info you want to keep track of alongside your geometry. Read more on stream global
|
||||
variables
|
||||
<a href="https://speckle.guide/user/web.html#globals" target="_blank">here</a>
|
||||
.
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<b>Global editor help:</b>
|
||||
Drag and drop fields in and out of groups as you please. Click the box icon next to
|
||||
any field to turn it into a nested group of fields.
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-alert
|
||||
v-if="!(stream.role === 'stream:contributor') && !(stream.role === 'stream:owner')"
|
||||
class="my-3"
|
||||
dense
|
||||
type="warning"
|
||||
>
|
||||
You are free to play around with the globals here, but you do not have the required
|
||||
stream permission to save your changes.
|
||||
</v-alert>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<!-- History -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-card
|
||||
v-if="!$apollo.loading"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} pa-0`"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<v-toolbar
|
||||
class="elevation-"
|
||||
flat
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
|
||||
>
|
||||
<v-toolbar-title style="cursor: pointer" @click="showHistory = !showHistory">
|
||||
<v-icon small class="mr-2">mdi-history</v-icon>
|
||||
Globals History ({{ branch.commits.totalCount }})
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon class="mr-1" @click="showHistory = !showHistory">
|
||||
<v-icon>
|
||||
{{ !showHistory ? 'mdi-chevron-down' : 'mdi-chevron-up' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list
|
||||
v-show="showHistory"
|
||||
v-if="branch.commits.totalCount !== 0"
|
||||
class="pa-0 transparent"
|
||||
dense
|
||||
>
|
||||
<list-item-commit
|
||||
v-for="item in branch.commits.items"
|
||||
:key="item.id"
|
||||
:route="`/streams/${streamId}/globals/${item.id}`"
|
||||
:commit="item"
|
||||
:stream-id="streamId"
|
||||
/>
|
||||
</v-list>
|
||||
<div v-else class="pa-2">No globals saved yet.</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8">
|
||||
<!-- Builder -->
|
||||
<globals-builder
|
||||
:branch-name="branchName"
|
||||
:stream-id="streamId"
|
||||
:object-id="objectId"
|
||||
:commit-message="commit ? commit.message : null"
|
||||
:user-role="stream.role"
|
||||
@new-commit="newCommit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import branchQuery from '@/graphql/branch.gql'
|
||||
|
||||
export default {
|
||||
name: 'Globals',
|
||||
components: {
|
||||
GlobalsBuilder: () => import('@/components/GlobalsBuilder'),
|
||||
ListItemCommit: () => import('@/components/ListItemCommit'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder')
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
},
|
||||
branch: {
|
||||
query: branchQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
branchName: this.branchName
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.stream.branch
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
branchName: 'globals', //TODO: handle multipile globals branches,
|
||||
revealBuilder: false,
|
||||
loading: false,
|
||||
showHistory: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamId() {
|
||||
return this.$route.params.streamId
|
||||
},
|
||||
commit() {
|
||||
return this.$route.params.commitId
|
||||
? this.branch?.commits?.items?.filter((c) => c.id == this.$route.params.commitId)[0]
|
||||
: this.branch?.commits?.items[0]
|
||||
},
|
||||
objectId() {
|
||||
return this.commit?.referencedObject
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createGlobals() {
|
||||
if (!this.branch) {
|
||||
this.loading = true
|
||||
this.$matomo && this.$matomo.trackPageView('globals/branch/create')
|
||||
this.$mixpanel.track('Globals Action', { type: 'action', name: 'create', hostApp: 'web' })
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation branchCreate($params: BranchCreateInput!) {
|
||||
branchCreate(branch: $params)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.streamId,
|
||||
name: 'globals',
|
||||
description: 'Stream globals'
|
||||
}
|
||||
}
|
||||
})
|
||||
this.$apollo.queries.branch.refetch()
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
this.revealBuilder = true
|
||||
},
|
||||
newCommit() {
|
||||
this.$apollo.queries.branch.refetch()
|
||||
if (this.$route.params.commitId) this.$router.push(`/streams/${this.streamId}/globals`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<v-row no-gutters>
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-1">mdi-database</v-icon>
|
||||
<span class="space-grotesk mr-2">Object View</span>
|
||||
</div>
|
||||
</portal>
|
||||
<v-col cols="12" sm="12" class="ma-0 pa-0">
|
||||
<v-card class="pa-0" elevation="0" rounded="lg" color="transparent" style="height: 60vh">
|
||||
<renderer :object-url="commitObjectUrl" @selection="handleSelection" />
|
||||
</v-card>
|
||||
<v-card class="pa-4" elevation="0" rounded="lg">
|
||||
<v-expand-transition>
|
||||
<v-sheet v-show="selectionData.length !== 0" class="pa-0" color="transparent">
|
||||
<v-card-title class="mr-8">
|
||||
<v-badge inline :content="selectionData.length">
|
||||
<v-icon class="mr-2">mdi-cube</v-icon>
|
||||
Selection
|
||||
</v-badge>
|
||||
</v-card-title>
|
||||
<div v-if="selectionData.length !== 0">
|
||||
<object-simple-viewer
|
||||
v-for="(obj, ind) in selectionData"
|
||||
:key="obj.id + ind"
|
||||
:value="obj"
|
||||
:stream-id="$route.params.streamId"
|
||||
:key-name="`Selected Object ${ind + 1}`"
|
||||
force-show-open-in-new
|
||||
force-expand
|
||||
/>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-expand-transition>
|
||||
<v-card-title class="mr-8">
|
||||
<v-icon class="mr-2">mdi-database</v-icon>
|
||||
Object {{ $route.params.objectId }}
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<object-speckle-viewer
|
||||
class="mt-4"
|
||||
:stream-id="$route.params.streamId"
|
||||
:value="commitObject"
|
||||
:expand="true"
|
||||
></object-speckle-viewer>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
import ObjectSpeckleViewer from '@/components/ObjectSpeckleViewer'
|
||||
import ObjectSimpleViewer from '@/components/ObjectSimpleViewer'
|
||||
import Renderer from '@/components/Renderer'
|
||||
|
||||
export default {
|
||||
name: 'ObjectViewer',
|
||||
components: { ObjectSimpleViewer, ObjectSpeckleViewer, Renderer },
|
||||
data() {
|
||||
return {
|
||||
selectionData: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
commitObject() {
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: this.$route.params.objectId
|
||||
}
|
||||
},
|
||||
commitObjectUrl() {
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}/objects/${this.$route.params.objectId}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelection(selectionData) {
|
||||
this.selectionData.splice(0, this.selectionData.length)
|
||||
this.selectionData.push(...selectionData)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<v-container style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-cog</v-icon>
|
||||
<span class="space-grotesk">Settings</span>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<v-alert v-if="stream.role !== 'stream:owner'" type="warning">
|
||||
Your permission level ({{ stream.role }}) is not high enough to edit this stream's details.
|
||||
</v-alert>
|
||||
|
||||
<v-card
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:loading="loading"
|
||||
>
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-cog</v-icon>
|
||||
<span class="d-inline-block">General</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="save">
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:rules="validation.nameRules"
|
||||
label="Name"
|
||||
hint="The name of this stream."
|
||||
class="mt-5"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="description"
|
||||
label="Description"
|
||||
hint="The description of this stream."
|
||||
class="mt-5"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="isPublic"
|
||||
inset
|
||||
class="mt-5"
|
||||
:label="isPublic ? 'Public (Link Sharing)' : 'Private'"
|
||||
:hint="
|
||||
isPublic
|
||||
? 'Anyone with the link can view this stream. It is also visible on your profile page. Only collaborators can push data to it.'
|
||||
: 'Only collaborators can access this stream.'
|
||||
"
|
||||
persistent-hint
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn class="ml-3" color="primary" type="submit" :disabled="!canSave" @click="save">
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} mt-2`"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-bomb</v-icon>
|
||||
<span class="d-inline-block">Danger Zone</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
<v-list-item three-line>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
color="error"
|
||||
fab
|
||||
dark
|
||||
small
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@click="deleteDialog = true"
|
||||
>
|
||||
<v-icon>mdi-delete-forever</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Permanently Delete Stream</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Once you delete a stream, there is no going back! All data will be removed, and
|
||||
existing collaborators will not be able to access it.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-dialog v-model="deleteDialog" width="500" @keydown.esc="deleteDialog = false">
|
||||
<v-card>
|
||||
<v-toolbar class="error mb-4">
|
||||
<v-toolbar-title>Deleting Stream '{{ stream.name }}'</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items>
|
||||
<v-btn icon @click="deleteDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Type the name of the stream below to confirm you really want to delete it. All data
|
||||
will be removed, and existing collaborators will not be able to access it.
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<b>You cannot undo this action.</b>
|
||||
|
||||
<v-text-field
|
||||
v-model="streamNameConfirm"
|
||||
label="Confirm stream name"
|
||||
class="pt-10"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!-- <v-btn text color="primary" @click="deleteDialog = false">Cancel</v-btn> -->
|
||||
<v-btn
|
||||
block
|
||||
class="mr-3"
|
||||
color="error"
|
||||
:disabled="streamNameConfirm !== stream.name"
|
||||
@click="deleteStream"
|
||||
>
|
||||
delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-snackbar v-model="snackbar" timeout="800" color="primary">
|
||||
<p class="text-center my-0">
|
||||
<b>Changes saved!</b>
|
||||
</p>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'StreamSettings',
|
||||
components: {},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
|
||||
update(data) {
|
||||
let stream = data.stream
|
||||
if (stream)
|
||||
({ name: this.name, description: this.description, isPublic: this.isPublic } = stream)
|
||||
|
||||
return stream
|
||||
}
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
loading: false,
|
||||
loadingDelete: false,
|
||||
valid: false,
|
||||
name: null,
|
||||
deleteDialog: false,
|
||||
streamNameConfirm: '',
|
||||
description: null,
|
||||
isPublic: true,
|
||||
validation: {
|
||||
nameRules: [(v) => !!v || 'A stream must have a name!']
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
canSave() {
|
||||
return (
|
||||
this.stream.role === 'stream:owner' &&
|
||||
this.valid &&
|
||||
(this.name !== this.stream.name ||
|
||||
this.description !== this.stream.description ||
|
||||
this.isPublic !== this.stream.isPublic)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
this.loading = true
|
||||
this.$matomo && this.$matomo.trackPageView('stream/update')
|
||||
this.$mixpanel.track('Stream Action', { type: 'action', name: 'update', hostApp: 'web' })
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation editDescription($input: StreamUpdateInput!) {
|
||||
streamUpdate(stream: $input)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.stream.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
isPublic: this.isPublic
|
||||
}
|
||||
}
|
||||
})
|
||||
this.snackbar = true
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
this.$apollo.queries.stream.refetch()
|
||||
this.loading = false
|
||||
},
|
||||
async deleteStream() {
|
||||
this.$matomo && this.$matomo.trackPageView('stream/delete')
|
||||
this.$mixpanel.track('Stream Action', { type: 'action', name: 'delete', hostApp: 'web' })
|
||||
this.loadingDelete = true
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation deleteStream($id: String!) {
|
||||
streamDelete(id: $id)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.stream.id
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
this.deleteDialog = false
|
||||
this.$router.push({ path: '/streams' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,832 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid pa-0 ma-0>
|
||||
<!-- Stream Page Navigation Drawer -->
|
||||
<v-navigation-drawer
|
||||
v-if="!error"
|
||||
v-model="streamNav"
|
||||
app
|
||||
fixed
|
||||
clipped
|
||||
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<!-- Toolbar holds link to stream home page -->
|
||||
<v-app-bar
|
||||
v-if="stream && $vuetify.breakpoint.smAndDown"
|
||||
style="position: absolute; top: 0; width: 100%; z-index: 90"
|
||||
elevation="0"
|
||||
flat
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<router-link
|
||||
v-tooltip="stream.name"
|
||||
:to="`/streams/${stream.id}`"
|
||||
class="text-decoration-none space-grotesk"
|
||||
>
|
||||
<v-icon class="mr-2 primary--text" style="font-size: 20px">mdi-folder</v-icon>
|
||||
<b>{{ stream.name }}</b>
|
||||
</router-link>
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- <v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader> -->
|
||||
|
||||
<!-- Top padding hack -->
|
||||
<div v-if="$vuetify.breakpoint.smAndDown" style="display: block; height: 65px"></div>
|
||||
<div v-if="!loggedIn" class="px-4 mt-2">
|
||||
<v-btn large block color="primary" to="/authn/login">Log In</v-btn>
|
||||
</div>
|
||||
<!-- Various Stream Details -->
|
||||
<v-card v-if="stream" elevation="0" class="pa-1 mb-0" color="transparent">
|
||||
<v-card-text class="caption">
|
||||
<span v-html="parsedDescription"></span>
|
||||
<router-link
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
:to="`/streams/${$route.params.streamId}/settings`"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
Edit
|
||||
</router-link>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<div class="caption">
|
||||
<span v-tooltip="formatDate(stream.createdAt)">
|
||||
Created
|
||||
<timeago :datetime="stream.createdAt"></timeago>
|
||||
</span>
|
||||
,
|
||||
<span v-tooltip="formatDate(stream.updatedAt)">
|
||||
Updated
|
||||
<timeago :datetime="stream.updatedAt"></timeago>
|
||||
</span>
|
||||
</div>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<div>
|
||||
<!--
|
||||
Note: the current layout fits either:
|
||||
- 5 x (collab avatars) + (manage collabs button), or
|
||||
- 4 x (collab avatars) + ( extra collabs info number ) + (manage collabs button)
|
||||
-->
|
||||
<user-avatar
|
||||
v-for="collab in stream.collaborators.slice(
|
||||
0,
|
||||
stream.collaborators.length > 5 ? 4 : 5
|
||||
)"
|
||||
:id="collab.id"
|
||||
:key="collab.id"
|
||||
:size="30"
|
||||
:avatar="collab.avatar"
|
||||
:name="collab.name"
|
||||
></user-avatar>
|
||||
<v-btn
|
||||
v-if="stream.collaborators.length > 5"
|
||||
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
|
||||
icon
|
||||
:to="`/streams/${stream.id}/collaborators`"
|
||||
>
|
||||
<span class="text-subtitle-1">+{{ stream.collaborators.length - 4 }}</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="stream.collaborators.length <= 5"
|
||||
v-tooltip="'Manage collaborators'"
|
||||
icon
|
||||
:to="`/streams/${stream.id}/collaborators`"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-avatar>
|
||||
<v-icon>mdi-account-plus</v-icon>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Your role: {{ stream.role }} -->
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<v-chip small class="mr-2">{{ stream.commits.totalCount }} Commits</v-chip>
|
||||
<v-chip small class="mr-2">{{ branchesTotalCount }} Branches</v-chip>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Stream menu options -->
|
||||
<v-list v-if="stream" style="padding-left: 10px" rounded dense class="mt-0 pt-0" expand>
|
||||
<v-list-item link :to="`/streams/${stream.id}`" class="no-overlay">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-home</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Stream Home</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Branch menu group -->
|
||||
<v-list-group v-model="branchMenuOpen" class="my-2">
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-source-branch</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>Branches ({{ branchesTotalCount }})</v-list-item-title>
|
||||
</template>
|
||||
<v-divider class="mb-1"></v-divider>
|
||||
<v-list-item
|
||||
v-if="stream.role !== 'stream:reviewer'"
|
||||
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
|
||||
link
|
||||
@click="showNewBranchDialog()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small style="padding-top: 10px" class="primary--text">mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>New Branch</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Create a new branch to help categorise your commits.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<!-- TODO -->
|
||||
<div v-if="!$apollo.queries.branchQuery.loading">
|
||||
<template v-for="(item, i) in groupedBranches">
|
||||
<v-list-item
|
||||
v-if="item.type === 'item'"
|
||||
:key="i"
|
||||
:to="`/streams/${stream.id}/branches/${item.name}`"
|
||||
exact
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="item.name !== 'main'" small style="padding-top: 10px">
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
<v-icon v-else small style="padding-top: 10px" class="primary--text">
|
||||
mdi-star
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.displayName }} ({{ item.commits.totalCount }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ item.description ? item.description : 'no description' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group
|
||||
v-else
|
||||
:key="i"
|
||||
sub-group
|
||||
:value="item.expand"
|
||||
prepend-icon=""
|
||||
:group="item.name"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item style="overflow: visible">
|
||||
<v-list-item-icon style="position: relative; left: -26px">
|
||||
<v-icon style="padding-top: 10px">
|
||||
{{ item.expand ? 'mdi-chevron-down' : 'mdi-chevron-down' }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content style="position: relative; left: -8px">
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ item.children.length }} branches
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item
|
||||
v-for="(kid, j) in item.children"
|
||||
:key="j"
|
||||
:to="`/streams/${stream.id}/branches/${kid.name}`"
|
||||
exact
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small style="padding-top: 10px">mdi-source-branch</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ kid.displayName }} ({{ kid.commits.totalCount }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ kid.description ? kid.description : 'no description' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader>
|
||||
<v-divider class="mb-2"></v-divider>
|
||||
</v-list-group>
|
||||
|
||||
<!-- Other menu items go here -->
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/globals`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-earth</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Globals</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/uploads`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-arrow-up</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Import IFC</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/webhooks`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-webhook</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Webhooks</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/collaborators`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-account-group</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Collaborators</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/settings`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Stream Page App Bar -->
|
||||
<v-app-bar
|
||||
v-if="!error"
|
||||
app
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
flat
|
||||
clipped-left
|
||||
>
|
||||
<v-app-bar-nav-icon v-if="true || !streamNav" @click="streamNav = !streamNav">
|
||||
<!-- <v-icon v-if="streamNav">mdi-chevron-left</v-icon> -->
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="pl-0">
|
||||
<router-link
|
||||
v-if="stream"
|
||||
v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)"
|
||||
class="text-decoration-none space-grotesk"
|
||||
:to="`/streams/${stream.id}`"
|
||||
>
|
||||
<b>{{ stream.name }}</b>
|
||||
</router-link>
|
||||
<span v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)" class="mx-2">/</span>
|
||||
<portal-target name="streamTitleBar" slim style="display: inline-block">
|
||||
<!-- child routes can teleport things here -->
|
||||
</portal-target>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<portal-target name="streamActionsBar">
|
||||
<!-- child routes can teleport buttons here -->
|
||||
</portal-target>
|
||||
<v-toolbar-items style="margin-right: -20px">
|
||||
<v-btn v-if="!loggedIn && stream && !streamNav" large color="primary" to="/authn/login">
|
||||
Log In
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="loggedIn && stream"
|
||||
v-tooltip="'Share this stream'"
|
||||
elevation="0"
|
||||
@click="openShareStreamDialog()"
|
||||
>
|
||||
<v-icon v-if="!stream.isPublic" small class="mr-2 grey--text">mdi-lock</v-icon>
|
||||
<v-icon v-else small class="mr-2 grey--text">mdi-lock-open</v-icon>
|
||||
<v-icon small class="mr-2">mdi-share-variant</v-icon>
|
||||
<span class="hidden-md-and-down">Share</span>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Stream Child Routes -->
|
||||
<v-container
|
||||
v-if="!error"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''}`"
|
||||
:class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`"
|
||||
fluid
|
||||
pt-0
|
||||
pr-0
|
||||
>
|
||||
<transition name="fade">
|
||||
<router-view v-if="stream" @refetch-branches="refetchBranches"></router-view>
|
||||
</transition>
|
||||
</v-container>
|
||||
<v-container v-else :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`">
|
||||
<error-placeholder :error-type="error.toLowerCase().includes('not found') ? '404' : 'access'">
|
||||
<h2>{{ error }}</h2>
|
||||
</error-placeholder>
|
||||
</v-container>
|
||||
|
||||
<branch-new-dialog ref="branchDialog" @refetch-branches="refetchBranches" />
|
||||
|
||||
<v-dialog v-model="shareStream" max-width="600" :fullscreen="$vuetify.breakpoint.xsOnly">
|
||||
<v-card>
|
||||
<v-sheet color="primary">
|
||||
<v-toolbar color="primary" dark flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-share-variant</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Engage Multiplayer Mode!</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="shareStream = false"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="mt-0 mb-0 px-2">
|
||||
<v-text-field
|
||||
ref="streamUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Stream url copied to clipboard. Use it in a connector, or just share it with colleagues!"
|
||||
style="color: blue"
|
||||
prepend-inner-icon="mdi-folder"
|
||||
:value="streamUrl"
|
||||
@focus="copyToClipboard"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-if="$route.params.branchName"
|
||||
ref="branchUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Branch url copied to clipboard. Most connectors can receive the latest commit from a branch by using this url."
|
||||
style="color: blue"
|
||||
prepend-inner-icon="mdi-source-branch"
|
||||
:value="streamUrl + '/branches/' + $route.params.branchName"
|
||||
@focus="copyToClipboard"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-if="$route.params.commitId"
|
||||
ref="commitUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Commit url copied to clipboard. Most connectors can receive a specific commit by using this url."
|
||||
style="color: blue"
|
||||
prepend-inner-icon="mdi-source-commit"
|
||||
:value="streamUrl + '/commits/' + $route.params.commitId"
|
||||
@focus="copyToClipboard"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
</v-sheet>
|
||||
<v-sheet
|
||||
v-if="stream"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
|
||||
>
|
||||
<v-toolbar v-if="stream.role === 'stream:owner'" class="transparent" rounded flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>{{ stream.isPublic ? 'mdi-lock-open' : 'mdi-lock' }}</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>
|
||||
{{ stream.isPublic ? 'Public stream' : 'Private stream' }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-switch
|
||||
v-model="stream.isPublic"
|
||||
inset
|
||||
class="mt-4"
|
||||
:loading="swapPermsLoading"
|
||||
@click="changeVisibility"
|
||||
></v-switch>
|
||||
</v-toolbar>
|
||||
<v-card-text v-if="stream.isPublic" class="pt-2">
|
||||
This stream is public. This means that anyone with the link can view and read data from
|
||||
it.
|
||||
</v-card-text>
|
||||
<v-card-text v-if="!stream.isPublic" class="pt-2 pb-2">
|
||||
This stream is private. This means that only collaborators can access it.
|
||||
</v-card-text>
|
||||
</v-sheet>
|
||||
<v-sheet v-if="stream">
|
||||
<v-toolbar
|
||||
v-tooltip="
|
||||
`${
|
||||
stream.role !== 'stream:owner'
|
||||
? 'You do not have the right access level (' +
|
||||
stream.role +
|
||||
') to add collaborators.'
|
||||
: ''
|
||||
}`
|
||||
"
|
||||
flat
|
||||
>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-account-group</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>
|
||||
Collaborators
|
||||
<user-avatar
|
||||
v-for="collab in stream.collaborators.slice(
|
||||
0,
|
||||
stream.collaborators.length > 5 ? 4 : 5
|
||||
)"
|
||||
:id="collab.id"
|
||||
:key="collab.id"
|
||||
:size="20"
|
||||
:avatar="collab.avatar"
|
||||
:name="collab.name"
|
||||
></user-avatar>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
rounded
|
||||
:to="`/streams/${$route.params.streamId}/collaborators`"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
>
|
||||
Manage
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-sheet>
|
||||
<v-sheet
|
||||
v-if="stream"
|
||||
:xxxclass="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
|
||||
>
|
||||
<v-toolbar
|
||||
v-if="!stream.isPublic"
|
||||
v-tooltip="
|
||||
`${
|
||||
stream.role !== 'stream:owner'
|
||||
? 'You do not have the right access level (' +
|
||||
stream.role +
|
||||
') to invite people to this stream.'
|
||||
: ''
|
||||
}`
|
||||
"
|
||||
flat
|
||||
class="transparent"
|
||||
>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-email</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Missing someone?</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
rounded
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@click="showStreamInviteDialog()"
|
||||
>
|
||||
Send Invite
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-sheet>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<stream-invite-dialog
|
||||
v-if="stream"
|
||||
ref="streamInviteDialog"
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-name="stream.name"
|
||||
/>
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
rounded="pill"
|
||||
:timeout="10000"
|
||||
style="z-index: 10000"
|
||||
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
|
||||
>
|
||||
<template v-if="snackbarInfo.type === 'commit'">
|
||||
<span>New commit created!</span>
|
||||
</template>
|
||||
<template v-if="snackbarInfo.type === 'branch'">
|
||||
<span>Branch "{{ snackbarInfo.name }}" created!</span>
|
||||
</template>
|
||||
|
||||
<template #action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="goToItemAndCloseSnackbar()">View</v-btn>
|
||||
<v-btn color="pink" icon v-bind="attrs" @click="snackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'Stream',
|
||||
components: {
|
||||
UserAvatar: () => import('@/components/UserAvatar'),
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
|
||||
BranchNewDialog: () => import('@/components/dialogs/BranchNewDialog'),
|
||||
StreamInviteDialog: () => import('@/components/dialogs/StreamInviteDialog')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
streamNav: true,
|
||||
error: '',
|
||||
snackbar: false,
|
||||
snackbarInfo: {},
|
||||
editStreamDialog: false,
|
||||
shareStream: false,
|
||||
branchMenuOpen: false,
|
||||
swapPermsLoading: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
role
|
||||
createdAt
|
||||
updatedAt
|
||||
description
|
||||
isPublic
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
if (err.message) this.error = err.message.replace('GraphQL error: ', '')
|
||||
else this.error = err
|
||||
}
|
||||
},
|
||||
branchQuery: {
|
||||
query: gql`
|
||||
query Stream($id: String!) {
|
||||
branchQuery: stream(id: $id) {
|
||||
id
|
||||
branches {
|
||||
totalCount
|
||||
items {
|
||||
name
|
||||
description
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
update: (data) => {
|
||||
// console.log(data.branchQuery.branches.items)
|
||||
return data.branchQuery
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
branchCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result(args) {
|
||||
if (!args.data.branchCreated) return
|
||||
this.snackbar = true
|
||||
this.snackbarInfo = { ...args.data.branchCreated, type: 'branch' }
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result(commitInfo) {
|
||||
if (!commitInfo.data.commitCreated) return
|
||||
console.log(commitInfo)
|
||||
this.snackbar = true
|
||||
this.snackbarInfo = { ...commitInfo.data.commitCreated, type: 'commit' }
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupedBranches() {
|
||||
if (!this.branchQuery) return
|
||||
let branches = this.branchQuery.branches.items
|
||||
let items = []
|
||||
for (let b of branches) {
|
||||
if (b.name === 'globals') continue
|
||||
let parts = b.name.split('/')
|
||||
if (parts.length === 1) {
|
||||
items.push({ ...b, displayName: b.name, type: 'item', children: [] })
|
||||
} else {
|
||||
let existing = items.find((i) => i.name === parts[0] && i.type === 'group')
|
||||
if (!existing) {
|
||||
existing = { name: parts[0], type: 'group', children: [], expand: false }
|
||||
items.push(existing)
|
||||
}
|
||||
existing.children.push({
|
||||
...b,
|
||||
displayName: parts.slice(1).join('/'),
|
||||
type: 'item'
|
||||
})
|
||||
if (this.$route.path.includes(b.name)) existing.expand = true
|
||||
}
|
||||
}
|
||||
let sorted = items.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase()
|
||||
const nameB = b.name.toLowerCase()
|
||||
if (nameA < nameB) return -1
|
||||
if (nameA > nameB) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return [
|
||||
...sorted.filter((it) => it.name === 'main'),
|
||||
...sorted.filter((it) => it.name !== 'main')
|
||||
]
|
||||
// return items
|
||||
},
|
||||
streamUrl() {
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}`
|
||||
},
|
||||
parsedDescription() {
|
||||
if (!this.stream || !this.stream.description) return 'No description provided.'
|
||||
return this.stream.description.replace(
|
||||
/\[(.+?)\]\((https?:\/\/[a-zA-Z0-9/.(]+?)\)/g,
|
||||
'<a href="$2" class="text-decoration-none" target="_blank">$1</a>'
|
||||
)
|
||||
},
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
},
|
||||
sortedBranches() {
|
||||
// TODO: group by `/` (for later)
|
||||
if (!this.branchQuery) return
|
||||
return [
|
||||
this.branchQuery.branches.items.find((b) => b.name === 'main'),
|
||||
...this.branchQuery.branches.items.filter((b) => b.name !== 'main' && b.name !== 'globals')
|
||||
]
|
||||
},
|
||||
branchesTotalCount() {
|
||||
if (!this.branchQuery) return 0
|
||||
return this.branchQuery.branches.items.filter((b) => b.name !== 'globals').length
|
||||
},
|
||||
userId() {
|
||||
return localStorage.getItem('uuid')
|
||||
},
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to) {
|
||||
// Ensures branch menu is open when navigating to a branch url
|
||||
if (to.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
|
||||
this.branchMenuOpen = true
|
||||
// closes any share dialog
|
||||
this.shareStream = false
|
||||
this.snackbar = false
|
||||
}
|
||||
// branchMenuOpen(val) {
|
||||
// if (this.$route.name.toLowerCase().includes('branch') && !val)
|
||||
// this.$nextTick(() => {
|
||||
// this.branchMenuOpen = true
|
||||
// })
|
||||
// }
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
function () {
|
||||
this.streamNav = !this.$vuetify.breakpoint.smAndDown
|
||||
}.bind(this),
|
||||
1
|
||||
)
|
||||
|
||||
// Ensures branch menu is open when navigating directly to a branch url
|
||||
this.branchMenuOpen = this.$route.name.toLowerCase().includes('branch')
|
||||
// Open stream invite dialog if ?invite=true (used by desktop connectors)
|
||||
if (this.$route.query.invite && this.$route.query.invite === 'true') {
|
||||
setTimeout(() => {
|
||||
this.$refs.streamInviteDialog.show()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToItemAndCloseSnackbar() {
|
||||
if (this.snackbarInfo.type === 'commit') {
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/commits/${this.snackbarInfo.id}`)
|
||||
} else if (this.snackbarInfo.type === 'branch') {
|
||||
this.$router.push(
|
||||
`/streams/${this.$route.params.streamId}/branches/${this.snackbarInfo.name}`
|
||||
)
|
||||
this.refetchBranches()
|
||||
}
|
||||
this.snackbar = false
|
||||
},
|
||||
copyToClipboard(e) {
|
||||
e.target.select()
|
||||
document.execCommand('copy')
|
||||
},
|
||||
openShareStreamDialog() {
|
||||
this.shareStream = true
|
||||
setTimeout(
|
||||
function () {
|
||||
// console.log(this.$refs.streamUrl.$refs.input)
|
||||
this.$refs.streamUrl.$refs.input.select()
|
||||
document.execCommand('copy')
|
||||
}.bind(this),
|
||||
100
|
||||
)
|
||||
},
|
||||
showStreamInviteDialog() {
|
||||
this.$refs.streamInviteDialog.show()
|
||||
},
|
||||
async changeVisibility() {
|
||||
this.swapPermsLoading = true
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation editDescription($input: StreamUpdateInput!) {
|
||||
streamUpdate(stream: $input)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.$route.params.streamId,
|
||||
isPublic: this.stream.isPublic
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
this.stream.isPublic = !this.stream.isPublic
|
||||
}
|
||||
this.swapPermsLoading = false
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
refetchBranches() {
|
||||
this.$apollo.queries.branchQuery.refetch()
|
||||
},
|
||||
showNewBranchDialog() {
|
||||
this.$refs.branchDialog.show()
|
||||
},
|
||||
formatDate(d) {
|
||||
if (!this.stream) return null
|
||||
let date = new Date(d)
|
||||
let options = { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
|
||||
return date.toLocaleString(undefined, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.no-overlay.v-list-item--active::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,229 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-container style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-arrow-up</v-icon>
|
||||
<span class="space-grotesk">Import IFC</span>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<v-card elevation="1" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'white' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-arrow-up</v-icon>
|
||||
<span class="d-inline-block">Import IFC Files - Alpha</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Speckle can now process IFC files and store them as a commit (snapshot). You can then
|
||||
access it from the Speckle API, and receive it in other applications.
|
||||
<!-- </v-card-text>
|
||||
<v-card-text> -->
|
||||
Thanks to the Open Source
|
||||
<a href="https://ifcjs.github.io/info/docs/Guide/web-ifc/Introduction" target="_blank">
|
||||
IFC.js Project
|
||||
</a>
|
||||
for making this possible.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-alert v-if="stream && (stream.role === 'stream:reviewer' || !stream.role)" type="warning">
|
||||
Your permission level ({{ stream.role ? stream.role : 'none' }}) is not high enough to
|
||||
access this feature.
|
||||
</v-alert>
|
||||
|
||||
<div v-if="stream && !(stream.role === 'stream:reviewer' || !stream.role)">
|
||||
<v-card
|
||||
elevation="0"
|
||||
color="transparent"
|
||||
class=""
|
||||
style="height: 220px; transition: all 0.2s ease"
|
||||
:class="`mt-4 mb-4 d-flex justify-center
|
||||
${dragover && !$vuetify.theme.dark ? 'grey lighten-4' : ''}
|
||||
${dragover && $vuetify.theme.dark ? 'grey darken-4' : ''}
|
||||
`"
|
||||
@drop.prevent="onFileDrop($event)"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragenter.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
>
|
||||
<div v-if="!dragError" class="align-self-center text-center">
|
||||
<input
|
||||
id="myid"
|
||||
type="file"
|
||||
accept=".ifc,.IFC"
|
||||
style="display: none"
|
||||
multiple
|
||||
@change="onFileSelect($event)"
|
||||
/>
|
||||
<v-icon
|
||||
x-large
|
||||
color="primary"
|
||||
:class="`hover-tada ${dragover ? 'tada' : ''}`"
|
||||
style="cursor: pointer"
|
||||
onclick="document.getElementById('myid').click()"
|
||||
>
|
||||
mdi-cloud-upload
|
||||
</v-icon>
|
||||
<br />
|
||||
<span class="primary--text">Drag and drop your IFC file here!</span>
|
||||
<br />
|
||||
<span class="caption">Maximum 5 files at a time. Size is restricted to 50mb each.</span>
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="dragError"
|
||||
dismissible
|
||||
class="align-self-center text-center"
|
||||
type="error"
|
||||
@click="dragError = null"
|
||||
>
|
||||
{{ dragError }}
|
||||
</v-alert>
|
||||
</v-card>
|
||||
|
||||
<!-- {{ uploads }} -->
|
||||
<template v-for="file in files">
|
||||
<file-upload-item
|
||||
:key="file.fileName"
|
||||
:file="file"
|
||||
:branches="stream.branches.items"
|
||||
@done="uploadCompleted"
|
||||
></file-upload-item>
|
||||
</template>
|
||||
</div>
|
||||
<v-card elevation="1" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'white' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<!-- <v-icon class="mr-2" small>mdi-arrow-up</v-icon> -->
|
||||
<span class="d-inline-block">Previous Uploads</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Here are the previously uploaded files in this stream. Please note, currently processing
|
||||
time is restricted to 5 minutes - if a file takes longer to process, it will be ignored.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<template v-for="file in streamUploads" v-if="!$apollo.loading">
|
||||
<file-processing-item :key="file.id" :file-id="file.id" />
|
||||
</template>
|
||||
<v-card v-if="!$apollo.loading && streamUploads.length === 0" class="my-4 elevation-1">
|
||||
<v-toolbar dense flat color="transparent">
|
||||
<v-toolbar-title>No uploads yet.</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'Webhooks',
|
||||
components: {
|
||||
FileUploadItem: () => import('@/components/FileUploadItem'),
|
||||
FileProcessingItem: () => import('@/components/FileProcessingItem')
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
role
|
||||
branches {
|
||||
totalCount
|
||||
items {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.$route.params.streamId }
|
||||
}
|
||||
},
|
||||
streamUploads: {
|
||||
query: gql`
|
||||
query streamUploads($streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
role
|
||||
fileUploads {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.stream.fileUploads,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragover: false,
|
||||
loading: false,
|
||||
stream: null,
|
||||
files: [],
|
||||
showUploadDialog: false,
|
||||
error: null,
|
||||
dragError: null
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onFileSelect(e) {
|
||||
this.parseFiles(e.target.files)
|
||||
},
|
||||
onFileDrop(e) {
|
||||
this.parseFiles(e.dataTransfer.files)
|
||||
},
|
||||
parseFiles(files) {
|
||||
this.dragover = false
|
||||
this.dragError = null
|
||||
for (const file of files) {
|
||||
console.log(file.name.split('.')[1])
|
||||
let extension = file.name.split('.')[1]
|
||||
if (!extension || extension !== 'ifc') {
|
||||
this.dragError = 'Only IFC file extensions are supported.'
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 50626997) {
|
||||
this.dragError = 'Your files are too powerful (for now). Maximum upload size is 50mb!'
|
||||
return
|
||||
}
|
||||
|
||||
if (this.files.findIndex((f) => f.name === file.name) !== -1) {
|
||||
this.dragError = 'This file is already primed for upload.'
|
||||
return
|
||||
}
|
||||
}
|
||||
if (files.length > 5) {
|
||||
this.dragError = 'Maximum five files at a time allowed.'
|
||||
return
|
||||
}
|
||||
this.dragError = null
|
||||
|
||||
for (const file of files) {
|
||||
this.files.push(file)
|
||||
}
|
||||
},
|
||||
uploadCompleted(file) {
|
||||
const index = this.files.findIndex((f) => f.name === file)
|
||||
this.files.splice(index, 1)
|
||||
this.$apollo.queries.streamUploads.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<no-data-placeholder
|
||||
v-if="!$apollo.loading && webhooks.length === 0 && stream && stream.role === 'stream:owner'"
|
||||
:show-message="false"
|
||||
>
|
||||
<h2>This stream has no webhooks.</h2>
|
||||
<p class="caption">
|
||||
Webhooks allow you to subscribe to a stream's events and get notified of them in real time.
|
||||
You can then use this to trigger ci apps, automation workflows, and more.
|
||||
</p>
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item link class="primary mb-4" dark @click="newWebhookDialog = true">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Create Webhook</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
|
||||
href="https://speckle.guide/dev/server-webhooks.html"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-book-open-variant</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Webhook docs</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Read the documentation on webhooks.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</no-data-placeholder>
|
||||
|
||||
<error-placeholder v-if="error" error-type="access">
|
||||
<h2>Only stream owners can access webhooks.</h2>
|
||||
<p class="caption">
|
||||
If you need to use webhooks, ask the stream's owner to grant you ownership.
|
||||
</p>
|
||||
</error-placeholder>
|
||||
|
||||
<v-container v-if="!$apollo.loading && webhooks.length !== 0" style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-webhook</v-icon>
|
||||
<span class="space-grotesk">Webhooks</span>
|
||||
</div>
|
||||
</portal>
|
||||
<v-card elevation="0" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-webhook</v-icon>
|
||||
<span class="d-inline-block">What are Webhooks?</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pb-1">
|
||||
<p class="caption">
|
||||
Webhooks allow you to subscribe to a stream's events and get notified of them in real
|
||||
time. You can then use this to trigger ci apps, automation workflows, and more. Read
|
||||
more on webhooks
|
||||
<a href="https://speckle.guide/dev/server-webhooks.html" target="_blank">here</a>
|
||||
.
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:loading="loading"
|
||||
:class="`mt-2 ${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-webhook</v-icon>
|
||||
<span class="d-inline-block">Existing Webhooks</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn small class="primary" dark @click="newWebhookDialog = true">New Webhook</v-btn>
|
||||
</v-toolbar>
|
||||
<v-list subheader class="transparent pa-0 ma-0">
|
||||
<v-list-item v-for="wh in webhooks" :key="wh.id" link style="cursor: default">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="wh.statusIcon.color" class="pt-2">
|
||||
{{ wh.statusIcon.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ wh.description ? wh.description : `Webhook ${wh.id}` }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ wh.url }} {{ `(${wh.triggers.join(', ')})` }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ getStatusInfo(wh) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="wh.history.items.length != 0">
|
||||
<v-btn
|
||||
v-tooltip="'View status reports'"
|
||||
icon
|
||||
@click="
|
||||
selectedWebhook = wh
|
||||
statusReportsDialog = true
|
||||
"
|
||||
>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
@click="
|
||||
selectedWebhook = wh
|
||||
editWebhookDialog = true
|
||||
"
|
||||
>
|
||||
edit
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
|
||||
<v-card-text v-if="webhooks && webhooks.length == 0">
|
||||
There are no webhooks on this stream yet.
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
color="primary"
|
||||
href="https://speckle.guide/dev/server-webhooks.html"
|
||||
target="_blank"
|
||||
>
|
||||
Read the docs
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
|
||||
<v-dialog v-model="newWebhookDialog" width="500" :fullscreen="$vuetify.breakpoint.smAndDown">
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Create Webhook</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn icon @click="newWebhookDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<webhook-form
|
||||
:loading.sync="loading"
|
||||
:stream-id="$attrs.streamId"
|
||||
@refetch-webhooks="refetchWebhooks"
|
||||
@close="newWebhookDialog = false"
|
||||
/>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="editWebhookDialog" width="500" :fullscreen="$vuetify.breakpoint.smAndDown">
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Edit Webhook</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn icon @click="editWebhookDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<webhook-form
|
||||
v-if="selectedWebhook"
|
||||
:loading.sync="loading"
|
||||
:stream-id="$attrs.streamId"
|
||||
:webhook-id="selectedWebhook.id"
|
||||
@refetch-webhooks="refetchWebhooks"
|
||||
@close="editWebhookDialog = false"
|
||||
/>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="statusReportsDialog" width="500" :fullscreen="$vuetify.breakpoint.smAndDown">
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Webhook Status Reports</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn icon @click="statusReportsDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list v-if="selectedWebhook">
|
||||
<v-subheader>Latest delivery reports:</v-subheader>
|
||||
<v-list-item v-for="(sr, index) in selectedWebhook.history.items" :key="index">
|
||||
<v-list-item-icon>
|
||||
<v-icon :class="`${sr.status === 2 ? 'green--text' : 'red--text'}`">
|
||||
{{ sr.status === 2 ? 'mdi-check' : 'mdi-close' }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<div>
|
||||
{{ sr.statusInfo }}
|
||||
</div>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Last update: {{ sr.lastUpdate }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import webhooksQuery from '@/graphql/webhooks.gql'
|
||||
export default {
|
||||
name: 'Webhooks',
|
||||
components: {
|
||||
WebhookForm: () => import('@/components/settings/WebhookForm'),
|
||||
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder')
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: webhooksQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
data.stream.webhooks.items.forEach((wh) => {
|
||||
wh.statusIcon = this.getStatusIcon(wh)
|
||||
})
|
||||
return data.stream
|
||||
},
|
||||
error(err) {
|
||||
if (err.message) this.error = err.message.replace('GraphQL error: ', '')
|
||||
else this.error = err
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
stream: null,
|
||||
newWebhookDialog: false,
|
||||
editWebhookDialog: false,
|
||||
statusReportsDialog: false,
|
||||
selectedWebhook: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
webhooks() {
|
||||
if (this.stream) return this.stream.webhooks.items
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatusIcon(webhook) {
|
||||
let status = 5 // default 5 if no events
|
||||
if (webhook.history.items.length) status = webhook.history.items[0].status
|
||||
switch (status) {
|
||||
case 0:
|
||||
case 1:
|
||||
return { color: 'amber', icon: 'mdi-alert-outline' }
|
||||
case 2:
|
||||
return { color: 'green', icon: 'mdi-check' }
|
||||
case 3:
|
||||
return { color: 'red', icon: 'mdi-close' }
|
||||
default:
|
||||
return { color: 'blue-grey', icon: 'mdi-alert-circle-outline' }
|
||||
}
|
||||
},
|
||||
getStatusInfo(webhook) {
|
||||
if (!webhook.history.items.length) return 'No events yet'
|
||||
let msg = webhook.history.items[0].statusInfo
|
||||
|
||||
return msg
|
||||
},
|
||||
refetchWebhooks() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user