feat(frontend): navigation refractor

This commit is contained in:
Matteo Cominetti
2021-07-28 18:15:29 +02:00
parent 71904ea82f
commit 052d3fe068
18 changed files with 987 additions and 814 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
<style type="text/css">
body {
@@ -0,0 +1,139 @@
<template>
<v-breadcrumbs
v-if="!$apollo.loading"
class="display-1"
:items="breadcrumbs"
divider="/"
></v-breadcrumbs>
</template>
<script>
import gql from 'graphql-tag'
export default {
name: 'BreadcrumbTitle',
apollo: {
stream: {
query: gql`
query Stream($id: String!) {
stream(id: $id) {
id
name
}
}
`,
variables() {
return {
id: this.$route.params.streamId
}
}
},
branch: {
query: gql`
query Stream($streamId: String!, $branchName: String!) {
stream(id: $streamId) {
id
name
branch(name: $branchName) {
id
name
}
}
}
`,
variables() {
return {
streamId: this.$route.params.streamId,
branchName: this.$route.params.branchName
}
},
skip() {
return !this.$route.params.branchName
},
update: (data) => {
return data.stream.branch
}
},
commit: {
query: gql`
query Stream($streamId: String!, $commitId: String!) {
stream(id: $streamId) {
id
name
commit(id: $commitId) {
id
branchName
}
}
}
`,
variables() {
return {
streamId: this.$route.params.streamId,
commitId: this.$route.params.commitId
}
},
skip() {
return !this.$route.params.commitId
},
update: (data) => {
return data.stream.commit
}
}
},
computed: {
breadcrumbs() {
let items = [
{
text: this.stream.name,
disabled: false,
exact: true,
to: '/streams/' + this.stream.id
}
]
if (this.branch) {
items.push({
text: 'branches',
disabled: false,
exact: true,
to: '/streams/' + this.stream.id + '/branches/'
})
items.push({
text: this.branch.name,
disabled: true,
exact: true,
to: '/streams/' + this.stream.id + '/branches/' + encodeURIComponent(this.branch.name)
})
} else if (this.commit) {
items.push({
text: 'branches',
disabled: false,
exact: true,
to: '/streams/' + this.stream.id + '/branches/'
})
items.push({
text: this.commit.branchName,
disabled: false,
exact: true,
to:
'/streams/' + this.stream.id + '/branches/' + encodeURIComponent(this.commit.branchName)
})
items.push({
text: this.commit.id,
disabled: true
})
} else {
items.push({
text: this.$route.name,
disabled: true
})
}
return items
}
}
}
</script>
<style scoped lang="scss"></style>
@@ -8,8 +8,8 @@
@close="closeSaveDialog"
/>
</v-dialog>
<v-card-title>Globals</v-card-title>
<v-card-subtitle v-if="commitMessage">
<b>Selected commit:</b>
<v-icon dense class="text-subtitle-1">mdi-source-commit</v-icon>
{{ commitMessage }}
</v-card-subtitle>
+66 -89
View File
@@ -8,7 +8,7 @@ const routes = [
path: '/authn',
name: 'Auth',
redirect: '/authn/login',
component: () => import('../views/Auth.vue'),
component: () => import('@/views/Auth.vue'),
children: [
{
path: 'login',
@@ -16,7 +16,7 @@ const routes = [
meta: {
title: 'Login | Speckle'
},
component: () => import('../views/auth/Login.vue')
component: () => import('@/views/auth/Login.vue')
},
{
path: 'register',
@@ -24,7 +24,7 @@ const routes = [
meta: {
title: 'Register | Speckle'
},
component: () => import('../views/auth/Registration.vue')
component: () => import('@/views/auth/Registration.vue')
},
{
path: 'resetpassword',
@@ -32,7 +32,7 @@ const routes = [
meta: {
title: 'Register | Speckle'
},
component: () => import('../views/auth/ResetPasswordRequest.vue')
component: () => import('@/views/auth/ResetPasswordRequest.vue')
},
{
path: 'resetpassword/finalize',
@@ -40,7 +40,7 @@ const routes = [
meta: {
title: 'Register | Speckle'
},
component: () => import('../views/auth/ResetPasswordFinalization.vue')
component: () => import('@/views/auth/ResetPasswordFinalization.vue')
},
{
path: 'verify/:appId/:challenge',
@@ -48,7 +48,7 @@ const routes = [
meta: {
title: 'Authorizing App | Speckle'
},
component: () => import('../views/auth/AuthorizeApp.vue')
component: () => import('@/views/auth/AuthorizeApp.vue')
}
]
},
@@ -57,7 +57,7 @@ const routes = [
meta: {
title: 'Home | Speckle'
},
component: () => import('../views/Frontend.vue'),
component: () => import('@/views/Frontend.vue'),
children: [
{
path: '',
@@ -65,7 +65,7 @@ const routes = [
meta: {
title: 'Home | Speckle'
},
component: () => import('../views/Timeline.vue')
component: () => import('@/views/Timeline.vue')
},
{
path: 'streams',
@@ -73,14 +73,14 @@ const routes = [
meta: {
title: 'Streams | Speckle'
},
component: () => import('../views/Streams.vue')
component: () => import('@/views/Streams.vue')
},
{
path: 'streams/:streamId',
meta: {
title: 'Stream | Speckle'
},
component: () => import('../views/Stream.vue'),
component: () => import('@/views/stream/Stream.vue'),
children: [
{
path: '',
@@ -88,31 +88,16 @@ const routes = [
meta: {
title: 'Stream | Speckle'
},
component: () => import('../views/StreamMain.vue')
},
// {
// path: 'globals/',
// name: 'globals',
// meta: {
// title: 'Globals | Speckle'
// },
// component: () => import('../views/Globals.vue')
// },
{
path: 'globals/:commitId',
name: 'previous globals',
meta: {
title: 'Globals | Speckle'
},
component: () => import('../views/Globals.vue')
component: () => import('@/views/stream/Activity.vue')
},
{
path: 'branches/',
name: 'branches',
meta: {
title: 'Branches | Speckle'
},
component: () => import('../views/Branches.vue')
component: () => import('@/views/stream/Branches.vue')
},
{
path: 'branches/:branchName',
@@ -120,15 +105,7 @@ const routes = [
meta: {
title: 'Branch | Speckle'
},
component: () => import('../views/StreamMain.vue')
},
{
path: 'branches/:branchName/commits',
name: 'commits',
meta: {
title: 'Commits | Speckle'
},
component: () => import('../views/Commits.vue')
component: () => import('@/views/stream/Branch.vue')
},
{
path: 'commits/:commitId',
@@ -136,7 +113,7 @@ const routes = [
meta: {
title: 'Commit | Speckle'
},
component: () => import('../views/Commit.vue')
component: () => import('@/views/stream/Commit.vue')
},
{
path: 'objects/:objectId',
@@ -144,55 +121,55 @@ const routes = [
meta: {
title: 'Object | Speckle'
},
component: () => import('../views/Object.vue')
component: () => import('@/views/stream/Object.vue')
},
{
path: 'settings/',
name: 'settings',
meta: {
title: 'Stream Settings | Speckle'
},
props: true,
component: () => import('../views/settings/StreamSettings.vue'),
component: () => import('@/views/stream/Settings.vue')
},
{
path: 'webhooks/',
name: 'webhooks',
meta: {
title: 'Webhooks | Speckle'
},
props: true,
component: () => import('@/views/stream/Webhooks.vue'),
children: [
{
path: 'general/',
name: 'general',
meta: {
title: 'Stream Settings | Speckle'
},
props: true,
component: () => import('../views/settings/SettingsGeneral.vue')
},
{
path: 'webhooks/',
name: 'webhooks',
meta: {
title: 'Webhooks | Speckle'
},
props: true,
component: () => import('../views/settings/SettingsWebhooks.vue'),
children: [
{
path: 'edit/:webhookId/',
name: 'edit webhook',
props: true
}
]
},
{
path: 'webhooks/new/',
name: 'add webhook',
props: true,
component: () => import('../views/settings/SettingsWebhooks.vue')
},
{
path: 'globals/',
name: 'globals',
meta: {
title: 'Globals | Speckle'
},
props: true,
component: () => import('../views/Globals.vue')
path: 'edit/:webhookId/',
name: 'edit webhook',
props: true
}
]
},
{
path: 'webhooks/new/',
name: 'add webhook',
props: true,
component: () => import('@/views/stream/Webhooks.vue')
},
{
path: 'globals/',
name: 'globals',
meta: {
title: 'Globals | Speckle'
},
props: true,
component: () => import('@/views/stream/Globals.vue')
},
{
path: 'globals/:commitId',
name: 'previous globals',
meta: {
title: 'Globals | Speckle'
},
component: () => import('@/views/stream/Globals.vue')
}
]
},
@@ -203,7 +180,7 @@ const routes = [
meta: {
title: 'Your Profile | Speckle'
},
component: () => import('../views/Profile.vue')
component: () => import('@/views/Profile.vue')
},
{
path: 'profile/:userId',
@@ -211,7 +188,7 @@ const routes = [
meta: {
title: 'User Profile | Speckle'
},
component: () => import('../views/ProfileUser.vue')
component: () => import('@/views/ProfileUser.vue')
},
{
path: 'admin',
@@ -222,25 +199,25 @@ const routes = [
{
name: 'Admin | Overview',
path: '',
component: () => import('../views/admin/AdminOverview.vue')
component: () => import('@/views/admin/AdminOverview.vue')
},
{
name: 'Admin | Users',
path: 'users',
component: () => import('../views/admin/AdminUsers.vue')
component: () => import('@/views/admin/AdminUsers.vue')
},
{
name: 'Admin | Streams',
path: 'streams',
component: () => import('../views/admin/AdminStreams.vue')
component: () => import('@/views/admin/AdminStreams.vue')
},
{
name: 'Admin | Settings',
path: 'settings',
component: () => import('../views/admin/AdminSettings.vue')
component: () => import('@/views/admin/AdminSettings.vue')
}
],
component: () => import('../views/admin/AdminPanel.vue')
component: () => import('@/views/admin/AdminPanel.vue')
}
]
},
@@ -250,7 +227,7 @@ const routes = [
meta: {
title: 'Error | Speckle'
},
component: () => import('../views/Error.vue')
component: () => import('@/views/Error.vue')
},
{
path: '/onboarding',
@@ -258,12 +235,12 @@ const routes = [
meta: {
title: 'Getting Started | Speckle'
},
component: () => import('../views/GettingStartedView.vue')
component: () => import('@/views/GettingStartedView.vue')
},
{
path: '/embed',
name: 'Embeded Viewer',
component: () => import('../views/EmbedViewer.vue')
component: () => import('@/views/EmbedViewer.vue')
},
{
path: '*',
@@ -271,7 +248,7 @@ const routes = [
meta: {
title: 'Not Found | Speckle'
},
component: () => import('../views/NotFound.vue')
component: () => import('@/views/NotFound.vue')
}
]
-139
View File
@@ -1,139 +0,0 @@
<template>
<v-container>
<div v-if="!objectId && !$apollo.loading && !revealBuilder">
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>You don't have any globals on this stream!</v-card-title>
<v-card-text
v-if="$attrs['user-role'] === 'contributor' || $attrs['user-role'] === 'owner'"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info you
want to keep track of alongside your geometry. Would you like to create some now?
</v-card-text>
<v-card-text
v-if="!($attrs['user-role'] === 'contributor') && !($attrs['user-role'] === 'owner')"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info you
want to keep track of alongside your geometry. You don't have permission to create and
edit globals on this stream, but you can create your own stream to try them out!
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
v-if="$attrs['user-role'] === 'contributor' || $attrs['user-role'] === 'owner'"
color="primary"
@click="createClicked"
>
create globals
</v-btn>
</v-card-actions>
</v-card>
</div>
<div v-if="objectId || revealBuilder">
<globals-builder
:branch-name="branchName"
:stream-id="streamId"
:object-id="objectId"
:commit-message="commit ? commit.message : null"
:user-role="$attrs['user-role']"
@new-commit="newCommit"
/>
<v-card v-if="!$apollo.loading && branch.commits.items.length">
<v-card-title>History</v-card-title>
<v-card-text>
<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-card-text>
</v-card>
</div>
</v-container>
</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')
},
apollo: {
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
}
},
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 createClicked() {
if (!this.branch) {
this.loading = true
this.$matomo && this.$matomo.trackPageView('globals/branch/create')
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>
-154
View File
@@ -1,154 +0,0 @@
<template>
<v-container :fluid="$vuetify.breakpoint.mdAndDown">
<!-- <v-navigation-drawer app clipped left>
<v-list>
<v-list-item v-for="n in 5" :key="n" link>
<v-list-item-content>
<v-list-item-title>Item {{ n }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer> -->
<v-row v-if="stream">
<v-col cols="12" sm="12" md="4" lg="3" xl="3">
<sidebar-stream :user-role="userRole"></sidebar-stream>
</v-col>
<v-col cols="12" sm="12" md="8" lg="9" xl="9" class="pt-10">
<router-view :user-role="userRole"></router-view>
</v-col>
</v-row>
<v-row v-else-if="error" justify="center">
<v-col cols="12" sm="12" md="8" lg="9" xl="8" class="pt-10">
<error-block :message="error" />
</v-col>
</v-row>
<v-snackbar
v-if="commitSnackbarInfo"
v-model="commitSnackbar"
:timeout="5000"
color="primary"
absolute
right
top
>
New commit
<i>{{ commitSnackbarInfo.message }}</i>
on
<i>{{ commitSnackbarInfo.branchName }}</i>
<template #action="{ attrs }">
<v-btn
text
v-bind="attrs"
:to="'/streams/' + $route.params.streamId + '/commits/' + commitSnackbarInfo.id"
@click="commitSnackbar = false"
>
see
</v-btn>
<v-btn icon v-bind="attrs" @click="commitSnackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
<stream-invite-dialog v-if="stream" ref="streamInviteDialog" :stream-id="stream.id" />
</v-container>
</template>
<script>
import SidebarStream from '../components/SidebarStream'
import ErrorBlock from '../components/ErrorBlock'
import streamQuery from '../graphql/stream.gql'
import gql from 'graphql-tag'
import StreamInviteDialog from '../components/dialogs/StreamInviteDialog'
export default {
name: 'Stream',
components: {
SidebarStream,
ErrorBlock,
StreamInviteDialog
},
data() {
return {
error: '',
commitSnackbar: false,
commitSnackbarInfo: {}
}
},
apollo: {
stream: {
query: streamQuery,
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($id: String!) {
streamUpdated(streamId: $id)
}
`,
variables() {
return {
id: this.$route.params.streamId
}
},
result(info) {
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(commitInfo) {
if (!commitInfo.data.commitCreated) return
this.commitSnackbar = true
this.commitSnackbarInfo = commitInfo.data.commitCreated
},
skip() {
return !this.loggedIn
}
}
}
},
computed: {
userRole() {
let uuid = localStorage.getItem('uuid')
if (!uuid) return null
if (this.$apollo.loading) return null
let contrib = this.stream.collaborators.find((u) => u.id === uuid)
if (contrib) return contrib.role.split(':')[1]
else return null
}
},
mounted() {
//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)
}
},
loggedIn() {
return localStorage.getItem('uuid') !== null
}
}
</script>
@@ -1,149 +0,0 @@
<template>
<admin-card v-if="selectedWebhook != undefined" :loading="loading" title="Edit Webhook">
<template #subtitle>
<v-icon dense class="text-subtitle-1 pr-1">mdi-webhook</v-icon>
<code>{{ selectedWebhook.id }}</code>
</template>
<webhook-form
:loading.sync="loading"
:stream-id="$attrs.streamId"
:webhook-id="selectedWebhook.id"
@refetch-webhooks="refetchWebhooks"
/>
</admin-card>
<admin-card v-else-if="$route.name === 'add webhook'" :loading="loading" title="Add Webhook">
<webhook-form
:loading.sync="loading"
:stream-id="$attrs.streamId"
@refetch-webhooks="refetchWebhooks"
/>
</admin-card>
<admin-card v-else title="Webhooks">
<template #menu>
<v-btn
small
outlined
color="primary"
:to="`/settings/streams/${$attrs.streamId}/webhooks/new`"
>
Add Webhook
</v-btn>
</template>
<v-card-text v-if="webhooks.length == 0">
You don't have any webhooks on this stream yet. Click the blue "Add Webhook" button in the top
right to add one.
</v-card-text>
<v-list subheader two-line>
<v-list-item
v-for="wh in webhooks"
:key="wh.id"
:to="`/settings/streams/${$attrs.streamId}/webhooks/edit/${wh.id}`"
>
<v-list-item-content>
<v-list-item-title>
<v-tooltip left>
<template #activator="{ on }" class="ml-1">
<v-icon class="pb-2 pr-1" small :color="wh.statusIcon.color" v-on="on">
{{ wh.statusIcon.icon }}
</v-icon>
</template>
<span>{{ getStatusInfo(wh) }}</span>
</v-tooltip>
<span id="description">
{{ wh.description ? wh.description : `webhook ${wh.id}` }}
</span>
</v-list-item-title>
<v-list-item-subtitle>{{ wh.url }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ `( ${wh.triggers.join(', ')} )` }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</admin-card>
</template>
<script>
import webhooksQuery from '../../graphql/webhooks.gql'
export default {
name: 'SettingsWebhooks',
components: {
AdminCard: () => import('@/components/admin/AdminCard'),
WebhookForm: () => import('@/components/settings/WebhookForm')
},
props: {
userRole: {
type: String,
default: null
}
},
apollo: {
webhooks: {
query: webhooksQuery,
variables() {
return {
streamId: this.$attrs.streamId
}
},
update(data) {
let webhooks = data.stream.webhooks.items
webhooks.forEach((wh) => {
wh.statusIcon = this.getStatusIcon(wh)
})
return webhooks
}
}
},
data() {
return {
loading: false
}
},
computed: {
selectedWebhook() {
if (this.$apollo.loading || !this.$attrs.webhookId) return
return this.webhooks.find(({ id }) => id === this.$attrs.webhookId)
}
},
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.webhooks.refetch()
}
}
}
</script>
<style>
#description {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 300px;
display: inline-block;
}
</style>
@@ -1,136 +0,0 @@
<template>
<v-container v-if="isOwner">
<v-row>
<v-col cols="12" sm="12" md="4" lg="3" xl="3" class="pt-md-10">
<v-card id="sideMenu" elevation="1" class="rounded-lg overflow-hidden">
<v-card-title class="tmr-8 display-1 text--secondary">
{{ stream.name }}
<br />
<v-btn block plain small class="justify-start mt-3 pa-0" :to="'/streams/' + stream.id">
<v-icon small>mdi-chevron-left</v-icon>
back to stream
</v-btn>
</v-card-title>
<div v-for="child in childRoutes" :key="child.to">
<router-link v-slot="{ isActive, navigate }" :to="child.to">
<v-hover v-slot="{ hover }">
<span
:class="{ 'active-border primary--text': isActive, 'primary--text': hover }"
class="pa-2 pl-6 text-left d-flex menu-item bold"
@click="navigate"
>
{{ child.name }}
</span>
</v-hover>
</router-link>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="12" md="8" lg="9" xl="9" class="pt-md-10">
<v-fade-transition mode="out-in">
<router-view :user-role="userRole" />
</v-fade-transition>
</v-col>
</v-row>
</v-container>
<v-container v-else-if="!isOwner && !$apollo.loading">
<v-card>
<v-card-text class="text-center">
<v-icon size="50" color="error">mdi-alert</v-icon>
<h3>Sorry...but maybe you shouldn't be here!</h3>
<p>
Either this stream does not exist or you do not have the required permissions to edit this
stream's settings.
</p>
<v-btn @click="$router.back()">Go back</v-btn>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import streamQuery from '../../graphql/stream.gql'
export default {
name: 'Settings',
components: {},
apollo: {
stream: {
query: streamQuery,
variables() {
return {
id: this.$attrs.streamId
}
},
error(err) {
if (err.message) this.error = err.message.replace('GraphQL error: ', '')
else this.error = err
}
}
},
data() {
return {
childRoutes: [
{
name: 'General',
to: `/settings/streams/${this.$attrs.streamId}/general`
},
{
name: 'Webhooks',
to: `/settings/streams/${this.$attrs.streamId}/webhooks`
},
{
name: 'Globals',
to: `/settings/streams/${this.$attrs.streamId}/globals`
}
]
}
},
computed: {
userRole() {
let uuid = localStorage.getItem('uuid')
if (!uuid) return null
if (this.$apollo.loading) return null
if (!this.stream) return null
let contrib = this.stream.collaborators.find((u) => u.id === uuid)
if (contrib) return contrib.role.split(':')[1]
else return null
},
isOwner() {
return this.userRole === 'owner'
}
},
methods: {}
}
</script>
<style lang="scss" scoped>
.gray-border {
border-top: 1pt solid var(--v-background-base) !important;
}
.menu-item {
overflow: hidden;
position: relative;
border-top: 1pt solid var(--v-background-base) !important;
cursor: pointer;
transition: 0.1s all ease-out, border-top-color 0s;
&::before {
@include speckle-gradient-bg;
position: absolute;
content: '';
width: 0;
height: 100%;
top: 0;
left: 0;
transition: all 0.1s ease-in-out, border-top-color 0s;
}
&.active-border::before {
width: 4pt;
}
}
</style>
@@ -1,5 +1,43 @@
<template>
<v-row v-if="!error">
<v-col v-if="stream">
<v-breadcrumbs class="display-1" :items="breadcrumbs" divider="/"></v-breadcrumbs>
<h3 class="title font-italic font-weight-thin my-5">
{{ truncate(stream.description) }}
</h3>
<div>
<v-chip>
<v-icon small>mdi-source-branch</v-icon>
{{ stream.branches.totalCount }}
branch{{ stream.branches.totalCount === 1 ? '' : 'es' }}
</v-chip>
<v-chip class="ml-3">
<v-icon small>mdi-source-commit</v-icon>
&nbsp;
{{ stream.commits.totalCount }}
commit{{ stream.commits.totalCount === 1 ? '' : 's' }}
</v-chip>
<v-chip class="ml-3">
<span
v-if="stream.isPublic"
v-tooltip="`Anyone can view this stream. Only you and collaborators can edit it.`"
>
<v-icon small>mdi-lock-open-variant-outline</v-icon>
&nbsp; public
</span>
<span v-else v-tooltip="`Only collaborators can access this stream.`">
<v-icon small>mdi-lock-outline</v-icon>
&nbsp; private
</span>
</v-chip>
<span class="ml-3 caption">
Created
<timeago v-tooltip="formatDate(stream.createdAt)" :datetime="stream.createdAt"></timeago>
</span>
</div>
</v-col>
<v-col sm="12">
<v-card v-if="$apollo.queries.branches.loading">
<v-skeleton-loader type="card-heading, card-avatar, article"></v-skeleton-loader>
@@ -208,7 +246,7 @@
</v-card>
<v-card
v-if="$apollo.queries.description.loading || $apollo.queries.branches.loading"
v-if="$apollo.queries.stream.loading || $apollo.queries.branches.loading"
class="mt-5"
>
<v-skeleton-loader type="article"></v-skeleton-loader>
@@ -222,17 +260,16 @@
</v-row>
</template>
<script>
import marked from 'marked'
import DOMPurify from 'dompurify'
import streamQuery from '@/graphql/stream.gql'
import gql from 'graphql-tag'
import NoDataPlaceholder from '../components/NoDataPlaceholder'
import SourceAppAvatar from '../components/SourceAppAvatar'
import streamBranchesQuery from '../graphql/streamBranches.gql'
import Renderer from '../components/Renderer'
import UserAvatar from '../components/UserAvatar'
import ErrorBlock from '../components/ErrorBlock'
import BranchNewDialog from '../components/dialogs/BranchNewDialog'
import BranchEditDialog from '../components/dialogs/BranchEditDialog'
import NoDataPlaceholder from '@/components/NoDataPlaceholder'
import SourceAppAvatar from '@/components/SourceAppAvatar'
import streamBranchesQuery from '@/graphql/streamBranches.gql'
import Renderer from '@/components/Renderer'
import UserAvatar from '@/components/UserAvatar'
import ErrorBlock from '@/components/ErrorBlock'
import BranchNewDialog from '@/components/dialogs/BranchNewDialog'
import BranchEditDialog from '@/components/dialogs/BranchEditDialog'
export default {
name: 'StreamMain',
@@ -273,21 +310,13 @@ export default {
return data.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
}
},
description: {
query: gql`
query($id: String!) {
stream(id: $id) {
id
description
}
}
`,
stream: {
query: streamQuery,
variables() {
return {
id: this.$route.params.streamId
}
},
update: (data) => data.stream.description
}
},
commitNotif: {
query: gql`
@@ -365,11 +394,6 @@ export default {
if (!this.branches) return []
return this.branches.map((b) => b.name)
},
compiledStreamDescription() {
if (!this.description) return ''
let md = marked(this.description)
return DOMPurify.sanitize(md)
},
latestCommit() {
if (!this.selectedBranch) return null
return this.selectedBranch.commits.items[0]
@@ -396,12 +420,22 @@ export default {
},
mounted() {
this.$apollo.queries.branches.refetch()
this.$apollo.queries.description.refetch()
this.$apollo.queries.stream.refetch()
},
methods: {
closeDescription() {
this.dialogDescription = false
this.$apollo.queries.description.refetch()
truncate(input, length = 250) {
if (!input) return ''
if (input.length > length) {
return input.substring(0, length) + '...'
}
return input
},
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)
},
editBranch() {
this.$refs.editBranchDialog.open(this.selectedBranch).then((dialog) => {
@@ -5,6 +5,7 @@
<v-skeleton-loader type="article, article"></v-skeleton-loader>
</v-col>
<v-col v-else-if="stream.branch" cols="12">
<breadcrumb-title />
<v-card class="pa-4" elevation="0" rounded="lg">
<branch-edit-dialog ref="editBranchDialog" />
@@ -15,7 +16,6 @@
<v-btn
v-if="userRole === 'contributor' || userRole === 'owner'"
small
plain
color="primary"
text
class="px-0"
@@ -25,7 +25,7 @@
Edit branch
</v-btn>
</v-card-title>
<v-breadcrumbs :items="breadcrumbs" divider="/"></v-breadcrumbs>
<v-card-text v-if="stream.branch.description">
{{ stream.branch.description }}
</v-card-text>
@@ -63,15 +63,17 @@
</template>
<script>
import gql from 'graphql-tag'
import branchQuery from '../graphql/branch.gql'
import ListItemCommit from '../components/ListItemCommit'
import BranchEditDialog from '../components/dialogs/BranchEditDialog'
import NoDataPlaceholder from '../components/NoDataPlaceholder'
import ErrorBlock from '../components/ErrorBlock'
import branchQuery from '@/graphql/branch.gql'
export default {
name: 'Branch',
components: { ListItemCommit, BranchEditDialog, NoDataPlaceholder, ErrorBlock },
components: {
ListItemCommit: () => import('@/components/ListItemCommit'),
BranchEditDialog: () => import('@/components/dialogs/BranchEditDialog'),
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
ErrorBlock: () => import('@/components/ErrorBlock'),
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
props: {
userRole: {
type: String,
@@ -114,32 +116,6 @@ export default {
computed: {
streamId() {
return this.$route.params.streamId
},
breadcrumbs() {
return [
{
text: this.stream.name,
disabled: false,
exact: true,
to: '/streams/' + this.stream.id
},
{
text: 'branches',
disabled: false,
exact: true,
to: '/streams/' + this.stream.id + '/branches/'
},
{
text: this.stream.branch.name,
disabled: true,
exact: true,
to:
'/streams/' +
this.stream.id +
'/branches/' +
encodeURIComponent(this.stream.branch.name)
}
]
}
},
methods: {
@@ -1,10 +1,11 @@
<template>
<v-row>
<v-col sm="12">
<v-card v-if="$apollo.queries.stream.loading">
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-card>
<v-card v-else rounded="lg" class="pa-4 mb-4" elevation="0">
<v-col v-if="$apollo.queries.stream.loading">
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-col>
<v-col v-else-if="branches" cols="12">
<breadcrumb-title />
<v-card rounded="lg" class="pa-4 mb-4" elevation="0">
<branch-new-dialog ref="newBranchDialog" />
<v-card-title>
@@ -14,7 +15,6 @@
<v-spacer />
<v-btn
v-if="userRole === 'contributor' || userRole === 'owner'"
plain
color="primary"
text
class="px-0"
@@ -25,7 +25,6 @@
New branch
</v-btn>
</v-card-title>
<v-breadcrumbs :items="breadcrumbs" divider="/"></v-breadcrumbs>
<v-card-text>
<i>
A branch represents an independent line of data. You can think of them as an independent
@@ -66,14 +65,14 @@
</v-row>
</template>
<script>
import BranchNewDialog from '../components/dialogs/BranchNewDialog'
import streamBranchesQuery from '../graphql/streamBranches.gql'
import streamBranchesQuery from '@/graphql/streamBranches.gql'
import gql from 'graphql-tag'
export default {
name: 'StreamMain',
components: {
BranchNewDialog
BranchNewDialog: () => import('@/components/dialogs/BranchNewDialog'),
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
props: {
userRole: {
@@ -139,22 +138,7 @@ export default {
branches() {
return this.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
},
breadcrumbs() {
return [
{
text: this.stream.name,
disabled: false,
exact: true,
to: '/streams/' + this.stream.id
},
{
text: 'branches',
disabled: true,
exact: true,
to: '/streams/' + this.stream.id + '/branches/'
}
]
},
loggedIn() {
return localStorage.getItem('uuid') !== null
}
@@ -7,6 +7,8 @@
</v-card>
</v-col>
<v-col v-else-if="stream.commit" cols="12">
<breadcrumb-title />
<v-card elevation="0" rounded="lg">
<v-sheet class="pa-4" color="transparent">
<commit-edit-dialog ref="commitDialog"></commit-edit-dialog>
@@ -18,17 +20,16 @@
v-if="userRole === 'contributor' || userRole === 'owner'"
v-tooltip="'Edit commit details'"
small
plain
color="primary"
text
class="px-0"
@click="editCommit"
>
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
Edit
Edit commit
</v-btn>
</v-card-title>
<v-breadcrumbs :items="breadcrumbs" divider="/"></v-breadcrumbs>
<v-list-item dense>
<v-list-item-icon class="mr-2 mt-1">
<user-avatar
@@ -107,25 +108,20 @@
</template>
<script>
import gql from 'graphql-tag'
import UserAvatar from '../components/UserAvatar'
import ObjectSpeckleViewer from '../components/ObjectSpeckleViewer'
import ObjectSimpleViewer from '../components/ObjectSimpleViewer'
import Renderer from '../components/Renderer'
import streamCommitQuery from '../graphql/commit.gql'
import CommitEditDialog from '../components/dialogs/CommitEditDialog'
import SourceAppAvatar from '../components/SourceAppAvatar'
import ErrorBlock from '../components/ErrorBlock'
import streamCommitQuery from '@/graphql/commit.gql'
export default {
name: 'Commit',
name: 'Branch',
components: {
CommitEditDialog,
UserAvatar,
ObjectSpeckleViewer,
ObjectSimpleViewer,
Renderer,
SourceAppAvatar,
ErrorBlock
CommitEditDialog: () => import('@/components/dialogs/CommitEditDialog'),
UserAvatar: () => import('@/components/UserAvatar'),
ObjectSpeckleViewer: () => import('@/components/ObjectSpeckleViewer'),
ObjectSimpleViewer: () => import('@/components/ObjectSimpleViewer'),
Renderer: () => import('@/components/Renderer'),
SourceAppAvatar: () => import('@/components/SourceAppAvatar'),
ErrorBlock: () => import('@/components/ErrorBlock'),
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
props: {
userRole: {
@@ -166,37 +162,6 @@ export default {
},
commitObjectUrl() {
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.commitObject.referencedId}`
},
breadcrumbs() {
return [
{
text: this.stream.name,
disabled: false,
exact: true,
to: '/streams/' + this.stream.id
},
{
text: 'branches',
disabled: false,
exact: true,
to: '/streams/' + this.stream.id + '/branches/'
},
{
text: this.stream.commit.branchName,
disabled: false,
exact: true,
to:
'/streams/' +
this.stream.id +
'/branches/' +
encodeURIComponent(this.stream.commit.branchName) +
'/commits'
},
{
text: this.stream.commit.message,
disabled: true
}
]
}
},
methods: {
@@ -0,0 +1,172 @@
<template>
<v-row>
<v-col>
<breadcrumb-title />
<div v-if="!objectId && !$apollo.loading && !revealBuilder">
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>You don't have any globals on this stream!</v-card-title>
<v-card-text
v-if="$attrs['user-role'] === 'contributor' || $attrs['user-role'] === 'owner'"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info
you want to keep track of alongside your geometry. Would you like to create some now?
</v-card-text>
<v-card-text
v-if="!($attrs['user-role'] === 'contributor') && !($attrs['user-role'] === 'owner')"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info
you want to keep track of alongside your geometry. You don't have permission to create
and edit globals on this stream, but you can create your own stream to try them out!
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
v-if="$attrs['user-role'] === 'contributor' || $attrs['user-role'] === 'owner'"
color="primary"
@click="createClicked"
>
create globals
</v-btn>
</v-card-actions>
</v-card>
</div>
<div v-if="objectId || revealBuilder">
<globals-builder
:branch-name="branchName"
:stream-id="streamId"
:object-id="objectId"
:commit-message="commit ? commit.message : null"
:user-role="$attrs['user-role']"
@new-commit="newCommit"
/>
<v-card v-if="!$apollo.loading && branch.commits.items.length">
<v-card-title>History</v-card-title>
<v-card-text>
<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-card-text>
</v-card>
</div>
</v-col>
</v-row>
</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'),
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
apollo: {
stream: {
query: gql`
query Stream($id: String!) {
stream(id: $id) {
id
name
}
}
`,
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
}
},
computed: {
breadcrumbs() {
return [
{
text: this.stream.name,
disabled: false,
exact: true,
to: '/streams/' + this.stream.id
},
{
text: 'globals',
disabled: true
}
]
},
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 createClicked() {
if (!this.branch) {
this.loading = true
this.$matomo && this.$matomo.trackPageView('globals/branch/create')
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>
@@ -43,9 +43,9 @@
</v-row>
</template>
<script>
import ObjectSpeckleViewer from '../components/ObjectSpeckleViewer'
import ObjectSimpleViewer from '../components/ObjectSimpleViewer'
import Renderer from '../components/Renderer'
import ObjectSpeckleViewer from '@/components/ObjectSpeckleViewer'
import ObjectSimpleViewer from '@/components/ObjectSimpleViewer'
import Renderer from '@/components/Renderer'
export default {
name: 'ObjectViewer',
@@ -0,0 +1,354 @@
<template>
<v-container :fluid="$vuetify.breakpoint.mdAndDown">
<v-navigation-drawer app clipped left>
<v-card-text v-if="stream">
<!-- <div v-if="isHomeRoute">
<v-btn
v-if="userRole === 'owner'"
small
color="primary"
text
class="px-0"
@click="editStreamDialog = true"
>
<v-icon small class="mr-2 float-left">mdi-pencil-outline</v-icon>
Edit details
</v-btn>
<v-btn
v-if="userRole === 'owner'"
small
color="primary"
text
class="px-0 d-block"
@click="dialogShare = true"
>
<v-icon small class="mr-2">mdi-account-multiple</v-icon>
Manage collaboratos
</v-btn> -->
<!-- <v-btn
v-tooltip="'Edit stream global variables!'"
small
plain
color="primary"
text
class="px-0 d-block justify-start"
:to="`/streams/${stream.id}/globals`"
>
Manage Globals
</v-btn> -->
<!-- <v-btn
v-if="userRole === 'owner'"
small
color="primary"
text
left
class="px-0"
:to="`/streams/${stream.id}/settings/general`"
@click.prevent=""
>
<v-icon small class="mr-2">mdi-cog-outline</v-icon>
Settings
</v-btn>
<v-btn
v-if="userRole === 'owner'"
small
plain
color="primary"
text
class="px-0 d-block"
@click="showStreamInviteDialog"
>
<v-icon small class="mr-2">mdi-email-send-outline</v-icon>
Invite to this stream
</v-btn>
<v-dialog v-model="editStreamDialog" max-width="500">
<stream-edit-dialog
:stream-id="stream.id"
:name="stream.name"
:description="stream.description"
:is-public="stream.isPublic"
:open="editStreamDialog"
@close="editClosed"
/>
</v-dialog>
<v-card-title><h5>Collaborators</h5></v-card-title>
<v-row no-gutters>
<template v-for="(collab, i) in stream.collaborators">
<v-col :key="i" cols="3" class="mb-2">
<user-avatar
:id="collab.id"
:size="40"
:avatar="collab.avatar"
:name="collab.name"
></user-avatar>
</v-col>
<v-col :key="collab.id" cols="9" class="mb-2 hidden-sm-and-down">
<span class="text-body-2">{{ collab.name }}</span>
<br />
<span class="caption">{{ collab.role.split(':')[1] }}</span>
</v-col>
</template>
</v-row>
<stream-invite-dialog ref="streamInviteDialog" :stream-id="stream.id" />
<v-dialog v-if="userId" v-model="dialogShare" max-width="500">
<stream-share-dialog
:users="stream.collaborators"
:stream-id="stream.id"
:user-id="userId"
@close="dialogShare = false"
></stream-share-dialog>
</v-dialog> -->
<!-- </div> -->
</v-card-text>
<v-list>
<v-list-item
v-for="menu in menues"
:key="menu.name"
:to="menu.to"
exact
@click="handleFunction(menu.click)"
>
<v-list-item-icon>
<v-icon>{{ menu.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ menu.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-row v-if="stream">
<!-- <v-col cols="12" sm="12" md="4" lg="3" xl="3">
<sidebar-stream :user-role="userRole"></sidebar-stream>
</v-col> -->
<v-col cols="12" class="pt-10">
<router-view :user-role="userRole"></router-view>
</v-col>
</v-row>
<v-row v-else-if="error" justify="center">
<v-col cols="12" class="pt-10">
<error-block :message="error" />
</v-col>
</v-row>
<v-snackbar
v-if="commitSnackbarInfo"
v-model="commitSnackbar"
:timeout="5000"
color="primary"
absolute
right
top
>
New commit
<i>{{ commitSnackbarInfo.message }}</i>
on
<i>{{ commitSnackbarInfo.branchName }}</i>
<template #action="{ attrs }">
<v-btn
text
v-bind="attrs"
:to="'/streams/' + $route.params.streamId + '/commits/' + commitSnackbarInfo.id"
@click="commitSnackbar = false"
>
see
</v-btn>
<v-btn icon v-bind="attrs" @click="commitSnackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
<stream-invite-dialog v-if="stream" ref="streamInviteDialog" :stream-id="stream.id" />
<v-dialog v-if="userId && stream" v-model="dialogShare" max-width="500">
<stream-share-dialog
:users="stream.collaborators"
:stream-id="stream.id"
:user-id="userId"
@close="dialogShare = false"
></stream-share-dialog>
</v-dialog>
<v-dialog v-model="editStreamDialog" max-width="500">
<stream-edit-dialog
v-if="stream"
:stream-id="stream.id"
:name="stream.name"
:description="stream.description"
:is-public="stream.isPublic"
:open="editStreamDialog"
@close="editClosed"
/>
</v-dialog>
</v-container>
</template>
<script>
import ErrorBlock from '@/components/ErrorBlock'
import streamQuery from '@/graphql/stream.gql'
import gql from 'graphql-tag'
import StreamInviteDialog from '@/components/dialogs/StreamInviteDialog'
import StreamEditDialog from '@/components/dialogs/StreamEditDialog'
import StreamShareDialog from '@/components/dialogs/StreamShareDialog'
export default {
name: 'Stream',
components: {
ErrorBlock,
StreamInviteDialog,
StreamShareDialog,
StreamEditDialog
},
data() {
return {
error: '',
commitSnackbar: false,
commitSnackbarInfo: {},
editStreamDialog: false,
dialogShare: false,
menues: [
{ name: 'Activity', icon: 'mdi-history', to: '/streams/' + this.$route.params.streamId },
{
name: 'Branches',
icon: 'mdi-source-branch',
to: '/streams/' + this.$route.params.streamId + '/branches'
},
{
name: 'Collaborators',
icon: 'mdi-account-group-outline',
click: 'manageCollabrators'
},
{
name: 'Globals',
icon: 'mdi-earth',
to: '/streams/' + this.$route.params.streamId + '/globals'
},
{
name: 'Webhooks',
icon: 'mdi-webhook',
to: '/streams/' + this.$route.params.streamId + '/webhooks'
},
{
name: 'Settings',
icon: 'mdi-cog-outline',
click: 'editStream'
}
]
}
},
apollo: {
stream: {
query: streamQuery,
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($id: String!) {
streamUpdated(streamId: $id)
}
`,
variables() {
return {
id: this.$route.params.streamId
}
},
result(info) {
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(commitInfo) {
if (!commitInfo.data.commitCreated) return
this.commitSnackbar = true
this.commitSnackbarInfo = commitInfo.data.commitCreated
},
skip() {
return !this.loggedIn
}
}
}
},
computed: {
userId() {
return localStorage.getItem('uuid')
},
userRole() {
let uuid = localStorage.getItem('uuid')
if (!uuid) return null
if (this.$apollo.loading) return null
let contrib = this.stream.collaborators.find((u) => u.id === uuid)
if (contrib) return contrib.role.split(':')[1]
else return null
}
},
mounted() {
//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)
}
},
loggedIn() {
return localStorage.getItem('uuid') !== null
},
methods: {
handleFunction(f) {
if (this[f]) this[f]()
},
editStream() {
this.editStreamDialog = true
},
manageCollabrators() {
this.dialogShare = true
},
showStreamInviteDialog() {
this.$refs.streamInviteDialog.show()
},
editClosed() {
this.editStreamDialog = false
this.$apollo.queries.stream.refetch()
}
}
}
</script>
<style>
.v-breadcrumbs {
padding: 0 !important;
margin-bottom: 25px;
}
.v-breadcrumbs li {
font-size: inherit !important;
font-weight: inherit !important;
}
</style>
@@ -0,0 +1,150 @@
<template>
<v-row>
<v-col>
<breadcrumb-title />
<admin-card v-if="selectedWebhook != undefined" :loading="loading" title="Edit Webhook">
<template #subtitle>
<v-icon dense class="text-subtitle-1 pr-1">mdi-webhook</v-icon>
<code>{{ selectedWebhook.id }}</code>
</template>
<webhook-form
:loading.sync="loading"
:stream-id="$attrs.streamId"
:webhook-id="selectedWebhook.id"
@refetch-webhooks="refetchWebhooks"
/>
</admin-card>
<admin-card v-else-if="$route.name === 'add webhook'" :loading="loading" title="Add Webhook">
<webhook-form
:loading.sync="loading"
:stream-id="$attrs.streamId"
@refetch-webhooks="refetchWebhooks"
/>
</admin-card>
<admin-card v-else>
<template #menu>
<v-btn small outlined color="primary" :to="`/streams/${$attrs.streamId}/webhooks/new`">
Add Webhook
</v-btn>
</template>
<v-card-text v-if="webhooks.length == 0">
You don't have any webhooks on this stream yet. Click the blue "Add Webhook" button in the
top right to add one.
</v-card-text>
<v-list subheader two-line>
<v-list-item
v-for="wh in webhooks"
:key="wh.id"
:to="`/settings/streams/${$attrs.streamId}/webhooks/edit/${wh.id}`"
>
<v-list-item-content>
<v-list-item-title>
<v-tooltip left>
<template #activator="{ on }" class="ml-1">
<v-icon class="pb-2 pr-1" small :color="wh.statusIcon.color" v-on="on">
{{ wh.statusIcon.icon }}
</v-icon>
</template>
<span>{{ getStatusInfo(wh) }}</span>
</v-tooltip>
<span id="description">
{{ wh.description ? wh.description : `webhook ${wh.id}` }}
</span>
</v-list-item-title>
<v-list-item-subtitle>{{ wh.url }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ `( ${wh.triggers.join(', ')} )` }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</admin-card>
</v-col>
</v-row>
</template>
<script>
import webhooksQuery from '@/graphql/webhooks.gql'
export default {
name: 'SettingsWebhooks',
components: {
AdminCard: () => import('@/components/admin/AdminCard'),
WebhookForm: () => import('@/components/settings/WebhookForm'),
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
props: {
userRole: {
type: String,
default: null
}
},
apollo: {
webhooks: {
query: webhooksQuery,
variables() {
return {
streamId: this.$attrs.streamId
}
},
update(data) {
let webhooks = data.stream.webhooks.items
webhooks.forEach((wh) => {
wh.statusIcon = this.getStatusIcon(wh)
})
return webhooks
}
}
},
data() {
return {
loading: false
}
},
computed: {
selectedWebhook() {
if (this.$apollo.loading || !this.$attrs.webhookId) return
return this.webhooks.find(({ id }) => id === this.$attrs.webhookId)
}
},
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.webhooks.refetch()
}
}
}
</script>
<style>
#description {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 300px;
display: inline-block;
}
</style>
@@ -15,7 +15,7 @@ module.exports = {
let stream = {
id: crs( { length: 10 } ),
name: name || 'Random Stream',
description: description || 'No description provided.',
description: description || '',
isPublic: isPublic !== false,
updatedAt: knex.fn.now( )
}