feat(ifc): frontend implementation + backend fixes/fiddles

it's still a dirty WIP
This commit is contained in:
Dimitrie Stefanescu
2021-10-06 12:25:18 +01:00
parent ba31300cde
commit b29b9302e1
17 changed files with 3899 additions and 168 deletions
-29
View File
@@ -1,29 +1,7 @@
const fs = require( 'fs' )
const fetch = require( 'node-fetch' )
const Parser = require( './parser' )
const ServerAPI = require( './api.js' )
// Hard coded local vars
const streamId = '27d29ef972'
// const branchName = 'main'
const userId = 'e24eb8e7e4'
// NOTE: not all the files below are present in the repo. Moreover, not all of the ones in the repo
// work properly, as we're dependent on the web-ifc library, whose support is partially limited, and/or
// the files are corrupt/do not pass validation. Welcome to IFC!
// const data = fs.readFileSync( './ifcs/20160414office_model_CV2_fordesign.ifc' )
// const data = fs.readFileSync( './ifcs/hospital.ifc' )
// const data = fs.readFileSync( './ifcs/primark.ifc' )
// const data = fs.readFileSync( './ifcs/231110AC11-Institute-Var-2-IFC.ifc' )
// const data = fs.readFileSync( './ifcs/small.ifc' )
// const data = fs.readFileSync( './ifcs/example.ifc' )
// const data = fs.readFileSync( './ifcs/steelplates.ifc' )
// const data = fs.readFileSync( './ifcs/piping.ifc' )
// const data = fs.readFileSync( './ifcs/railing.ifc' )
// const data = fs.readFileSync( './ifcs/hall.ifc' )
// const data = fs.readFileSync( './ifcs/231110ADT-FZK-Haus-2005-2006.ifc' )
// const data = fs.readFileSync( './ifcs/crazy.ifc' )
async function parseAndCreateCommit( { data, streamId, branchName = 'uploads', userId, message = 'Manual IFC file upload' } ) {
const serverApi = new ServerAPI( { streamId } )
const myParser = new Parser( { serverApi } )
@@ -34,7 +12,6 @@ async function parseAndCreateCommit( { data, streamId, branchName = 'uploads', u
streamId: streamId,
branchName: branchName,
objectId: id,
// authorId: userId, // not needed anymore (using raw api call for this)
message: message,
sourceApplication: 'IFC',
totalChildrenCount: tCount
@@ -74,10 +51,4 @@ async function parseAndCreateCommit( { data, streamId, branchName = 'uploads', u
return json.data.commitCreate
}
// parseAndCreateCommit( {
// data,
// streamId,
// userId
// } )
module.exports = { parseAndCreateCommit }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@
"url": "git+https://github.com/specklesystems/speckle-server.git"
},
"scripts": {
"dev": "S3_BUCKET=speckle-server nodemon ./src/daemon.js"
"dev": "PG_CONNECTION_STRING=postgresql://localhost:5432/speckle2_dev S3_BUCKET=galeata S3_ENDPOINT=ams3.digitaloceanspaces.com S3_ACCESS_KEY=44NG2WB3MY6J4VIP6HZW S3_SECRET_KEY=p78hHnry9GsxjNxXDdDCOqZmql4DElqxtrtGtdD5kU0 nodemon ./src/daemon.js"
},
"bugs": {
"url": "https://github.com/specklesystems/speckle-server/issues"
+2 -2
View File
@@ -56,7 +56,7 @@ async function doTask( task ) {
await new Promise( fulfill => diskFileStream.on( 'finish' , fulfill ) )
serverApi = new ServerAPI( { streamId: info.streamId } )
let { token } = await serverApi.createToken( { userId: info.userId, name: 'temp upload token', scopes: [ 'streams:write' ], lifespan: 3000 } )
let { token } = await serverApi.createToken( { userId: info.userId, name: 'temp upload token', scopes: [ 'streams:write', 'streams:read' ], lifespan: 1000000 } )
tempUserToken = token
await runProcessWithTimeout(
@@ -112,7 +112,7 @@ async function doTask( task ) {
}
function runProcessWithTimeout( cmd, cmdArgs, extraEnv, timeoutMs ) {
function runProcessWithTimeout( cmd, cmdArgs, extraEnv, timeoutMs ) {
return new Promise( ( resolve, reject ) => {
console.log( `Starting process: ${cmd} ${cmdArgs}` )
@@ -0,0 +1,109 @@
<template>
<v-card class="my-4 elevation-1" :loading="$apollo.loading">
<div v-if="!$apollo.loading && file">
<v-toolbar dense flat color="transparent">
<v-app-bar-nav-icon>
<v-icon>mdi-download</v-icon>
</v-app-bar-nav-icon>
<v-toolbar-title>
{{ file.fileName }}
</v-toolbar-title>
<v-spacer></v-spacer>
<template v-if="file.convertedStatus === 0">
<v-btn text disabled>
<span class="mr-2">Queued</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
</v-btn>
</template>
<template v-if="file.convertedStatus === 1">
<v-btn text>
<span class="mr-2">Converting</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
</v-btn>
</template>
<template v-if="file.convertedStatus === 2">
<v-btn
text
color="primary"
:to="`/streams/${$route.params.streamId}/commits/${file.convertedCommitId}`"
>
<span class="mr-2">View Commit</span>
<v-icon class="">mdi-open-in-new</v-icon>
</v-btn>
</template>
<template v-if="file.convertedStatus === 3">
<v-btn v-tooltip="file.convertedMessage" text>
<span class="mr-2 error--text">Error</span>
<v-icon color="error">mdi-bug</v-icon>
</v-btn>
</template>
</v-toolbar>
</div>
<div v-else>
<v-skeleton-loader
class="mx-auto"
max-width="300"
type="list-item-one-line"
></v-skeleton-loader>
</div>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
fileId: {
type: String,
default: null
}
},
data: () => ({
percentCompleted: -1,
error: null,
file: null
}),
apollo: {
file: {
query: gql`
query File($id: String!, $streamId: String!) {
stream(id: $streamId) {
id
fileUpload(id: $id) {
id
convertedCommitId
userId
convertedStatus
convertedMessage
fileName
fileType
uploadComplete
uploadDate
convertedLastUpdate
}
}
}
`,
variables() {
return {
id: this.fileId,
streamId: this.$route.params.streamId
}
},
skip() {
return !this.fileId
},
update: (data) => data.stream.fileUpload
}
},
watch: {
file(val) {
if (val.convertedStatus >= 2) this.$apollo.queries.file.stopPolling()
}
},
mounted() {
this.$apollo.queries.file.startPolling(1000)
},
methods: {}
}
</script>
@@ -0,0 +1,101 @@
<template>
<v-card v-if="file" class="my-4 elevation-1" :loading="percentCompleted != -1">
<template slot="progress">
<v-progress-linear color="primary" height="4" :value="percentCompleted"></v-progress-linear>
</template>
<v-toolbar flat color="transparent">
<v-toolbar-title>
{{ file.name }}
<span class="caption">{{ file.size }}kb</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn @click="upload()">Upload</v-btn>
</v-toolbar>
<v-alert v-if="error" type="error" dismissible>An error occurred.</v-alert>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: ['file'],
data: () => ({
percentCompleted: -1,
error: null
}),
apollo: {
streams: {
query: gql`
query Streams($query: String) {
streams(query: $query) {
totalCount
cursor
items {
id
name
updatedAt
}
}
}
`,
variables() {
return {
query: this.search
}
},
skip() {
return !this.search || this.search.length < 3
},
debounce: 300
}
},
watch: {},
methods: {
upload() {
let data = new FormData()
this.error = null
data.append('file', this.file)
let request = new XMLHttpRequest()
request.open('POST', `/api/file/ifc/${this.$route.params.streamId}`)
request.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('AuthToken')}`)
request.upload.addEventListener(
'progress',
function (e) {
this.percentCompleted = (e.loaded / e.total) * 100
if (this.percentCompleted >= 100) {
this.$emit('done', this.file.name)
}
}.bind(this)
)
// request finished event
request.addEventListener(
'load',
function () {
if (request.status !== 200) {
this.error = request.response
}
this.$emit('done', this.file.name)
}.bind(this)
)
request.addEventListener(
'error',
function () {
if (request.status !== 200) {
this.error = request.response
}
}.bind(this)
)
try {
request.send(data)
} catch (e) {
this.error = 'There was an error: ' + e
}
}
}
}
</script>
@@ -3,10 +3,10 @@
<v-row justify="center" style="margin-top: 50px" dense>
<v-col cols="12" lg="6" md="6" xl="6" class="d-flex flex-column justify-center align-center">
<v-card flat tile color="transparent" class="pa-0">
<div class="d-flex flex-column justify-space-between align-center mb-10">
<div class="d-flex flex-column justify-space-between align-center mb-10" v-if="showImage">
<v-img contain max-height="200" src="@/assets/emptybox.png"></v-img>
</div>
<div class=" text-center mb-2 space-grotesk">
<div class="text-center mb-2 space-grotesk">
<slot name="default"></slot>
</div>
<v-container style="max-width: 500px">
@@ -48,12 +48,12 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`${hasManager ? 'primary' : ''} mb-4`"
dark
href="https://speckle.systems/features/connectors"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-swap-horizontal</v-icon>
@@ -67,11 +67,11 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
href="https://speckle.systems/tutorials"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-school</v-icon>
@@ -85,11 +85,11 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
href="https://speckle.guide"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-book-open-variant</v-icon>
@@ -130,6 +130,12 @@
<script>
import gql from 'graphql-tag'
export default {
props: {
showImage: {
type: Boolean,
default: true
}
},
apollo: {
user: {
query: gql`
@@ -146,7 +152,7 @@ export default {
}
},
data() {
return{}
return {}
},
computed: {
rootUrl() {
@@ -160,14 +166,13 @@ export default {
mounted() {
this.checkAccountTimer = setInterval(
function () {
if(!this.hasManager)
this.$apollo.queries.user.refetch()
if (!this.hasManager) this.$apollo.queries.user.refetch()
}.bind(this),
3000
)
},
beforeDestroy() {
clearInterval( this.checkAccountTimer )
clearInterval(this.checkAccountTimer)
},
methods: {}
}
+10 -1
View File
@@ -94,7 +94,7 @@ const routes = [
{
path: 'branches/',
name: 'branches',
redirect: 'branches/main',
redirect: 'branches/main'
},
{
path: 'branches/:branchName',
@@ -156,6 +156,15 @@ const routes = [
props: true,
component: () => import('@/views/stream/Webhooks.vue')
},
{
path: 'uploads/',
name: 'uploads',
meta: {
title: 'Stream Uploads | Speckle'
},
props: true,
component: () => import('@/views/stream/Uploads.vue')
},
{
path: 'globals/',
name: 'globals',
@@ -1,41 +1,41 @@
<template>
<div id="admin-settings">
<v-card rounded="lg" v-if="serverInfo">
<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 class="d-flex align-center mb-2" v-for="(value, name) in serverDetails" :key="name">
<div class="flex-grow-1">
<div v-if="value.type == 'boolean'">
<p class="mt-2">{{value.label}}</p>
<v-switch
inset
persistent-hint
<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]"
class="pa-1 ma-1 caption"
>
<template v-slot:label>
<span class="caption">{{ value.hint }}</span>
</template>
</v-switch>
persistent-hint
:hint="value.hint"
class="ma-0 body-2"
></v-text-field>
</div>
<v-text-field
persistent-hint
v-else
:hint="value.hint"
v-model="serverModifications[name]"
class="ma-0 body-2"
></v-text-field>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn block color="primary" @click="saveEdit" :loading="loading">Save</v-btn>
</v-card-actions>
</v-card>
</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>
@@ -1,5 +1,5 @@
<template>
<v-container style="max-width: 768px;">
<v-container style="max-width: 768px">
<portal to="streamTitleBar">
<div>
<v-icon small class="mr-2 hidden-xs-only">mdi-account-multiple</v-icon>
@@ -7,62 +7,64 @@
</div>
</portal>
<v-alert type="warning" v-if="stream.role !== 'stream:owner'">
<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-card v-if="serverInfo" elevation="0" color="transparent" :class="`mb-4 py-4`">
<v-row align="stretch">
<v-col cols="12" sm="4" v-for="role in roles" :key="role.name">
<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-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-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"
:key="user.id"
: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>
<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"
:key="user.id"
: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>
<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"
:key="user.id"
:id="user.id"
:key="user.id"
:avatar="user.avatar"
:name="user.name"
:size="30"
/>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
@@ -79,9 +81,9 @@
</template>
<v-toolbar
v-if="stream.role === 'stream:owner'"
flat
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
v-if="stream.role === 'stream:owner'"
>
<v-toolbar-title>
<v-icon small class="mr-2">mdi-account-plus</v-icon>
@@ -152,9 +154,9 @@
: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-if="stream.role === 'stream:owner'"
>
<v-toolbar-title>
<v-icon small class="mr-2">mdi-account-group</v-icon>
@@ -179,8 +181,8 @@
item-value="name"
:items="roles"
class="py-0 my-0"
@change="setUserPermissions(user)"
:disabled="stream.role !== 'stream:owner'"
@change="setUserPermissions(user)"
>
<template #selection="{ item }">
{{ item.name }}
@@ -201,8 +203,8 @@
icon
small
color="error"
@click="removeUser(user)"
:disabled="stream.role !== 'stream:owner'"
@click="removeUser(user)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
@@ -294,10 +296,10 @@ export default {
}
},
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'
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
@@ -368,4 +370,4 @@ export default {
}
}
}
</script>
</script>
@@ -2,13 +2,13 @@
<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"
v-model="streamNav"
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
v-if="!error"
>
<!-- Toolbar holds link to stream home page -->
<v-app-bar
@@ -19,9 +19,9 @@
>
<v-toolbar-title>
<router-link
v-tooltip="stream.name"
:to="`/streams/${stream.id}`"
class="text-decoration-none space-grotesk"
v-tooltip="stream.name"
>
<v-icon class="mr-2 primary--text" style="font-size: 20px">mdi-folder</v-icon>
<b>{{ stream.name }}</b>
@@ -32,18 +32,18 @@
<!-- <v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader> -->
<!-- Top padding hack -->
<div style="display: block; height: 65px" v-if="$vuetify.breakpoint.smAndDown"></div>
<div class="px-4 mt-2" v-if="!loggedIn">
<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 elevation="0" v-if="stream" class="pa-1 mb-0" color="transparent">
<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"
v-if="stream.role === 'stream:owner'"
>
Edit
</router-link>
@@ -78,19 +78,19 @@
: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`"
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
v-if="stream.collaborators.length > 5"
>
<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-tooltip="'Manage collaborators'"
v-if="stream.collaborators.length <= 5"
>
<v-avatar>
<v-icon>mdi-account-plus</v-icon>
@@ -106,7 +106,7 @@
</v-card>
<!-- Stream menu options -->
<v-list style="padding-left: 10px" rounded dense class="mt-0 pt-0" v-if="stream">
<v-list v-if="stream" style="padding-left: 10px" rounded dense class="mt-0 pt-0">
<v-list-item link :to="`/streams/${stream.id}`" class="no-overlay">
<v-list-item-icon>
<v-icon small>mdi-home</v-icon>
@@ -119,7 +119,7 @@
<!-- Branch menu group -->
<!-- TODO: group by "/", eg. dim/a, dim/b, dim/c should be under a sub-group called "dim". -->
<v-list-group v-model="branchMenuOpen" class="my-2">
<template v-slot:activator>
<template #activator>
<v-list-item-icon>
<v-icon small>mdi-source-branch</v-icon>
</v-list-item-icon>
@@ -127,9 +127,9 @@
</template>
<v-divider class="mb-1"></v-divider>
<v-list-item
link
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
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>
@@ -144,17 +144,17 @@
</v-list-item>
<v-list-item
v-if="!$apollo.queries.branchQuery.loading"
v-for="(branch, i) in sortedBranches"
v-if="!$apollo.queries.branchQuery.loading"
:key="i"
link
:to="`/streams/${stream.id}/branches/${branch.name}`"
>
<v-list-item-icon>
<v-icon small style="padding-top: 10px" v-if="branch.name !== 'main'">
<v-icon v-if="branch.name !== 'main'" small style="padding-top: 10px">
mdi-source-branch
</v-icon>
<v-icon small style="padding-top: 10px" class="primary--text" v-else>mdi-star</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>
@@ -180,6 +180,15 @@
</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>
@@ -211,25 +220,25 @@
<!-- Stream Page App Bar -->
<v-app-bar
v-if="!error"
app
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
flat
clipped-left
v-if="!error"
>
<v-app-bar-nav-icon @click="streamNav = !streamNav" v-if="true || !streamNav">
<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"
v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)"
class="text-decoration-none space-grotesk"
:to="`/streams/${stream.id}`"
>
<b>{{ stream.name }}</b>
</router-link>
<span class="mx-2" v-show="true || !streamNav && !$vuetify.breakpoint.smAndDown">/</span>
<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>
@@ -239,17 +248,17 @@
<!-- child routes can teleport buttons here -->
</portal-target>
<v-toolbar-items style="margin-right: -20px">
<v-btn large color="primary" to="/authn/login" v-if="!loggedIn && stream && !streamNav">
<v-btn v-if="!loggedIn && stream && !streamNav" large color="primary" to="/authn/login">
Log In
</v-btn>
<v-btn
elevation="0"
v-if="loggedIn && stream"
@click="openShareStreamDialog()"
v-tooltip="'Share this stream'"
elevation="0"
@click="openShareStreamDialog()"
>
<v-icon small class="mr-2 grey--text" v-if="!stream.isPublic">mdi-lock</v-icon>
<v-icon small class="mr-2 grey--text" v-else>mdi-lock-open</v-icon>
<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>
@@ -258,18 +267,18 @@
<!-- 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
v-if="!error"
>
<transition name="fade">
<router-view v-if="stream" @refetch-branches="refetchBranches"></router-view>
</transition>
</v-container>
<v-container :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" v-else>
<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>
@@ -290,8 +299,8 @@
</v-toolbar>
<v-card-text class="mt-0 mb-0 px-2">
<v-text-field
dark
ref="streamUrl"
dark
filled
rounded
hint="Stream url copied to clipboard. Use it in a connector, or just share it with colleagues!"
@@ -302,8 +311,8 @@
></v-text-field>
<v-text-field
v-if="$route.params.branchName"
dark
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."
@@ -314,8 +323,8 @@
></v-text-field>
<v-text-field
v-if="$route.params.commitId"
dark
ref="commitUrl"
dark
filled
rounded
hint="Commit url copied to clipboard. Most connectors can receive a specific commit by using this url."
@@ -327,10 +336,10 @@
</v-card-text>
</v-sheet>
<v-sheet
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
v-if="stream"
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
>
<v-toolbar class="transparent" rounded v-if="stream.role === 'stream:owner'" flat>
<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>
@@ -339,9 +348,9 @@
</v-toolbar-title>
<v-spacer></v-spacer>
<v-switch
v-model="stream.isPublic"
inset
class="mt-4"
v-model="stream.isPublic"
:loading="swapPermsLoading"
@click="changeVisibility"
></v-switch>
@@ -356,7 +365,6 @@
</v-sheet>
<v-sheet v-if="stream">
<v-toolbar
flat
v-tooltip="
`${
stream.role !== 'stream:owner'
@@ -366,6 +374,7 @@
: ''
}`
"
flat
>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-account-group</v-icon>
@@ -401,8 +410,6 @@
:xxxclass="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
>
<v-toolbar
flat
class="transparent"
v-if="!stream.isPublic"
v-tooltip="
`${
@@ -413,6 +420,8 @@
: ''
}`
"
flat
class="transparent"
>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-email</v-icon>
@@ -423,8 +432,8 @@
color="primary"
text
rounded
@click="showStreamInviteDialog()"
:disabled="stream.role !== 'stream:owner'"
@click="showStreamInviteDialog()"
>
Send Invite
</v-btn>
@@ -0,0 +1,220 @@
<template>
<div>
<no-data-placeholder v-if="false" :show-image="false">
<h2>Import IFC Files</h2>
<p class="caption">
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.
</p>
<template #actions>
<v-list rounded class="transparent">
<v-list-item link class="primary mb-4" dark @click="showUploadDialog = true">
<v-list-item-icon>
<v-icon>mdi-plus-box</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Upload IFC File</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>Release Announcement</v-list-item-title>
<v-list-item-subtitle class="caption"></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 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-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">
<v-icon x-large color="primary" :class="`hover-tada ${dragover ? 'tada' : ''}`">
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"
@done="uploadCompleted"
></file-upload-item>
</template>
<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-container>
</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
name: 'Webhooks',
components: {
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
FileUploadItem: () => import('@/components/FileUploadItem'),
FileProcessingItem: () => import('@/components/FileProcessingItem')
},
apollo: {
streamUploads: {
query: gql`
query streamUploads($streamId: String!) {
stream(id: $streamId) {
id
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: {
onFileDrop(e) {
this.dragover = false
this.dragError = null
for (const file of e.dataTransfer.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 (e.dataTransfer.files.length > 5) {
this.dragError = 'Maximum five files at a time allowed.'
return
}
this.dragError = null
for (const file of e.dataTransfer.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>
+12 -14
View File
@@ -1,15 +1,15 @@
<template>
<div>
<no-data-placeholder
:show-message="false"
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 v-slot:actions>
<template #actions>
<v-list rounded class="transparent">
<v-list-item link class="primary mb-4" dark @click="newWebhookDialog = true">
<v-list-item-icon>
@@ -39,24 +39,22 @@
</v-list>
</template>
</no-data-placeholder>
<error-placeholder error-type="access" v-if="error">
<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>
<p class="caption">
If you need to use webhooks, ask the stream's owner to grant you ownership.
</p>
</error-placeholder>
<v-container style="max-width: 768px" v-if="!$apollo.loading && webhooks.length !== 0">
<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-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>
@@ -85,7 +83,7 @@
<span class="d-inline-block">Existing Webhooks</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn @click="newWebhookDialog = true" small class="primary" dark>New Webhook</v-btn>
<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">
@@ -107,12 +105,12 @@
</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
"
icon
v-tooltip="'View status reports'"
>
<v-icon>mdi-information</v-icon>
</v-btn>
@@ -0,0 +1,13 @@
const { getStreamFileUploads, getFileInfo } = require( '../../services/fileuploads' )
module.exports = {
Stream: {
async fileUploads( parent, args, context, info ) {
return await getStreamFileUploads( { streamId:parent.id } )
},
async fileUpload( parent, args, context, info ) {
return await getFileInfo( { fileId: args.id } )
}
}
}
@@ -0,0 +1,39 @@
extend type Stream {
"""
Returns a list of all the file uploads for this stream.
"""
fileUploads: [FileUpload]
"""
Returns a specific file upload that belongs to this stream.
"""
fileUpload(id:String!): FileUpload
}
type FileUpload {
id: String!
streamId: String!
branchName: String
"""
If present, the conversion result is stored in this commit.
"""
convertedCommitId: String
"""
The user's id that uploaded this file.
"""
userId: String!
"""
0 = queued, 1 = processing, 2 = success, 3 = error
"""
convertedStatus: Int!
"""
Holds any errors or info.
"""
convertedMessage: String
fileName: String!
fileType: String!
fileSize: Int!
uploadComplete: Boolean!
uploadDate: DateTime!
convertedLastUpdate: DateTime!
}
+5 -3
View File
@@ -49,7 +49,7 @@ exports.init = async ( app, options ) => {
return { hasPermissions: true, httpErrorCode: 200 }
}
app.get( '/api/download_file/:fileId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
app.get( '/api/file/:fileId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
if ( process.env.DISABLE_FILE_UPLOADS ) {
return res.status( 503 ).send( 'File uploads are disabled on this server' )
}
@@ -90,7 +90,7 @@ exports.init = async ( app, options ) => {
fileStream.pipe( res )
} ),
app.post( '/api/upload_file/:fileType/:streamId/:branchName?', contextMiddleware, matomoMiddleware, async ( req, res ) => {
app.post( '/api/file/:fileType/:streamId/:branchName?', contextMiddleware, matomoMiddleware, async ( req, res ) => {
if ( process.env.DISABLE_FILE_UPLOADS ) {
return res.status( 503 ).send( 'File uploads are disabled on this server' )
}
@@ -100,7 +100,8 @@ exports.init = async ( app, options ) => {
}
let fileUploadPromises = []
var busboy = new Busboy( { headers: req.headers } )
let busboy = new Busboy( { headers: req.headers } )
busboy.on( 'file', function( fieldname, file, filename, encoding, mimetype ) {
let promise = uploadFile( {
streamId: req.params.streamId,
@@ -112,6 +113,7 @@ exports.init = async ( app, options ) => {
} )
fileUploadPromises.push( promise )
} )
busboy.on( 'finish', async function() {
let fileIds = []
@@ -47,6 +47,11 @@ module.exports = {
return fileInfo
},
async getStreamFileUploads( { streamId } ) {
let fileInfos = await FileUploads().where( { streamId: streamId } ).select( '*' ).orderBy( [ { column: 'uploadDate', order: 'desc' } ] )
return fileInfos
},
async getFileStream( { fileId } ) {
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET