Merge remote-tracking branch 'origin/main' into alan/admin-panel

This commit is contained in:
Alan Rynne
2021-06-09 22:14:35 +02:00
40 changed files with 1717 additions and 336 deletions
+13 -5
View File
@@ -33,7 +33,7 @@ workflows:
context: main-builds
filters:
branches:
only: cristi/ci-test
only: cristi/ci-k8s-tor
jobs:
test_server:
@@ -90,13 +90,21 @@ jobs:
command: env SPECKLE_SERVER_PACKAGE=preview-service ./.circleci/build.sh
- run:
name: Deploy
command: ./.circleci/deploy.sh
command: |
./.circleci/deploy.sh
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_RELEASE} ./.circleci/deploy_in_new_setup.sh
else
env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_LATEST} ./.circleci/deploy_in_new_setup.sh
fi
- run:
name: Test deployment
command: |
./utils/test-deployment/install_prerequisites.sh
SPECKLE_SERVER=https://latest.speckle.dev
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
SPECKLE_SERVER=https://speckle.xyz
./utils/test-deployment/run_tests.py https://speckle.xyz
./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_RELEASE}
else
./utils/test-deployment/run_tests.py https://latest.speckle.dev
./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_LATEST}
fi
./utils/test-deployment/run_tests.py $SPECKLE_SERVER
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
set -e
K8S_CLUSTER_CERTIFICATE_VARIABLE=K8S_${K8S_CLUSTER}_CERTIFICATE
K8S_CLUSTER_CERTIFICATE=${!K8S_CLUSTER_CERTIFICATE_VARIABLE}
K8S_TOKEN_VARIABLE=K8S_${K8S_CLUSTER}_TOKEN
K8S_TOKEN=${!K8S_TOKEN_VARIABLE}
K8S_SERVER_VARIABLE=K8S_${K8S_CLUSTER}_SERVER
K8S_SERVER=${!K8S_SERVER_VARIABLE}
# K8S_NAMESPACE
IMAGE_VERSION_TAG=$CIRCLE_SHA1
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
IMAGE_VERSION_TAG=$CIRCLE_TAG
fi
echo "$K8S_CLUSTER_CERTIFICATE" | base64 --decode > k8s_cert.crt
# Update deployments
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
set image deployment/speckle-frontend main=$DOCKER_IMAGE_TAG-frontend:$IMAGE_VERSION_TAG
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
set image deployment/speckle-server main=$DOCKER_IMAGE_TAG-server:$IMAGE_VERSION_TAG
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
set image deployment/speckle-preview-service main=$DOCKER_IMAGE_TAG-preview-service:$IMAGE_VERSION_TAG
# Wait for rollout to complete
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
rollout status -w deployment/speckle-frontend --timeout=3m
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
rollout status -w deployment/speckle-server --timeout=3m
./kubectl \
--kubeconfig=/dev/null \
--server=$K8S_SERVER \
--certificate-authority=k8s_cert.crt \
--token=$K8S_TOKEN \
--namespace=$K8S_NAMESPACE \
rollout status -w deployment/speckle-preview-service --timeout=3m
+13
View File
@@ -12963,6 +12963,11 @@
"is-plain-obj": "^1.0.0"
}
},
"sortablejs": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
},
"source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -14821,6 +14826,14 @@
"@seregpie/claw": "^3.0.0"
}
},
"vuedraggable": {
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
"requires": {
"sortablejs": "1.10.2"
}
},
"vuetify": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.0.tgz",
+1
View File
@@ -29,6 +29,7 @@
"vue-matomo": "^3.14.0-0",
"vue-router": "^3.4.9",
"vue-timeago": "^5.1.2",
"vuedraggable": "^2.24.3",
"vuetify": "^2.3.21",
"vuetify-image-input": "^19.1.0",
"vuex": "^3.6.0"
@@ -0,0 +1,292 @@
<template>
<v-card rounded="lg" class="pa-3 mb-3" elevation="0">
<v-dialog v-model="saveDialog" max-width="500">
<globals-save-dialog
:branch-name="branchName"
:stream-id="$route.params.streamId"
:commit-obj="globalsCommit"
@close="closeSaveDialog"
/>
</v-dialog>
<v-card-title>Globals</v-card-title>
<v-card-subtitle v-if="commitMessage">
<v-icon dense class="text-subtitle-1">mdi-source-commit</v-icon>
{{ commitMessage }}
</v-card-subtitle>
<v-card-text>
These global variables can be used for storing design values, project requirements, notes, or
any info you want to keep track of alongside your geometry. Variable values can be text, numbers,
lists, or booleans. Click the box icon next to any field to turn it into a nested group of
fields, and drag and drop fields in and out of groups as you please! Note that field order
may not always be preserved.
</v-card-text>
<v-card-text v-if="!(userRole === 'contributor') && !(userRole === 'owner')">
You are free to play around with the globals here, but you do not have the required stream
permission to save your changes.
</v-card-text>
<v-card-actions>
<v-switch
v-model="deleteEntries"
v-tooltip="'Toggle delete mode'"
class="ml-3"
dense
inset
color="error"
:label="`DELETE`"
></v-switch>
<v-spacer />
<v-btn v-tooltip="'Clear all globals'" color="primary" small @click="clearGlobals">
clear
</v-btn>
<v-btn v-tooltip="'Undo any changes'" color="primary" small @click="resetGlobals">
reset all
</v-btn>
<v-btn
v-if="userRole === 'contributor' || userRole === 'owner'"
v-tooltip="'Save your changes with a message'"
small
:disabled="!globalsAreValid"
color="primary"
@click="
saveDialog = true
deleteEntries = false
"
>
save
</v-btn>
</v-card-actions>
<v-card-text>
<globals-entry
v-if="!$apollo.loading"
:entries="globalsArray"
:path="[]"
:remove="deleteEntries"
@add-prop="addProp"
@remove-prop="removeProp"
@field-to-object="fieldToObject"
@object-to-field="objectToField"
/>
<div v-else>
<v-skeleton-loader type="list-item-three-line" />
</div>
</v-card-text>
</v-card>
</template>
<script>
import crs from 'crypto-random-string'
import objectQuery from '../graphql/objectSingle.gql'
export default {
name: 'GlobalsBuilder',
components: {
GlobalsEntry: () => import('../components/GlobalsEntry'),
GlobalsSaveDialog: () => import('../components/dialogs/GlobalsSaveDialog')
},
apollo: {
object: {
query: objectQuery,
variables() {
return {
streamId: this.streamId,
id: this.objectId
}
},
update(data) {
delete data.stream.object.data.__closure
this.globalsArray = this.nestedGlobals(data.stream.object.data)
return data.stream.object
},
skip() {
return this.objectId == null
}
}
},
props: {
userRole: {
type: String,
default: null
},
branchName: {
type: String,
default: null
},
objectId: {
type: String,
default: null
},
streamId: {
type: String,
default: null
},
commitMessage: {
type: String,
default: null
}
},
data() {
return {
globalsArray: [],
globalsAreValid: true,
saveDialog: false,
deleteEntries: false,
sample: {
Region: 'London',
'Project Code': 'GB123456',
'Linked Projects': ['GB654321', 'EU424242'],
'Project Lead': 'Sir Spockle II',
'Pretty Cool?': true,
Climate: {
'Summer DBT [C]': 35,
'Summer WBT [C]': 20,
'Winter DBT [C]': -4,
'Winter WBT [C]': -4,
Enthalpy: {
'Summer Enthalpy [kJ/kg]': 56.87,
'Winter Enthalpy [kJ/kg]': 2.74
}
}
}
}
},
computed: {
globalsCommit() {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.globalsAreValid = true
let base = this.globalsToBase(this.globalsArray)
return base
}
},
mounted() {
if (!this.objectId) {
this.globalsArray = this.nestedGlobals(this.sample)
}
},
methods: {
nestedGlobals(data) {
if (!data) return []
let entries = Object.entries(data)
let arr = []
for (let [key, val] of entries) {
if (key.startsWith('__')) continue
if (['totalChildrenCount', 'speckle_type', 'id'].includes(key)) continue
if (!Array.isArray(val) && typeof val === 'object' && val !== null) {
if (val.speckle_type && val.speckle_type === 'reference') {
arr.push({
key,
valid: true,
id: crs({ length: 10 }),
value: val,
globals: this.nestedGlobals(val),
type: 'object' //TODO: handle references
})
} else {
arr.push({
key,
valid: true,
id: crs({ length: 10 }),
value: val,
globals: this.nestedGlobals(val),
type: 'object'
})
}
} else {
arr.push({
key,
valid: true,
id: crs({ length: 10 }),
value: val,
type: 'field'
})
}
}
return arr
},
globalsToBase(arr) {
let base = {
// eslint-disable-next-line camelcase
speckle_type: 'Base',
id: null
}
for (let entry of arr) {
if (!entry.value && !entry.globals) return
if (!entry.valid) this.globalsAreValid = false
if (Array.isArray(entry.value)) base[entry.key] = entry.value
else if (entry.type == 'object') {
base[entry.key] = this.globalsToBase(entry.globals)
} else if (typeof entry.value === 'string' && entry.value.includes(',')) {
base[entry.key] = entry.value
.replace(/\s/g, '')
.split(',')
.map((el) => (isNaN(el) ? el : parseFloat(el)))
} else if (typeof entry.value === 'boolean') {
base[entry.key] = entry.value
} else {
base[entry.key] = isNaN(entry.value) ? entry.value : parseFloat(entry.value)
}
}
return base
},
resetGlobals() {
this.deleteEntries = false
this.globalsArray = this.object?.data
? this.nestedGlobals(this.object.data)
: this.nestedGlobals(this.sample)
},
clearGlobals() {
this.globalsArray = this.nestedGlobals({ placeholder: 'something cool goes here...' })
},
addProp(kwargs) {
let globals = this.getNestedGlobals(kwargs.path)
globals.splice(globals.length, 0, kwargs.field)
},
removeProp(kwargs) {
let globals = this.getNestedGlobals(kwargs.path)
globals.splice(kwargs.index, 1)
},
fieldToObject(kwargs) {
let globals = this.getNestedGlobals(kwargs.path)
globals.splice(kwargs.index, 1, kwargs.obj)
},
objectToField(kwargs) {
let globals = this.getNestedGlobals(kwargs.path)
globals.splice(kwargs.index, 1, ...kwargs.fields)
},
getNestedGlobals(path) {
let entry = this.globalsArray
if (!path) return entry
let depth = path.length
if (depth > 0) {
let id = path.shift()
entry = entry.find((e) => e.id == id)
}
if (depth > 1) {
path.forEach((id) => {
entry = entry.globals.find((e) => e.id == id)
})
}
if (!Array.isArray(entry)) entry = entry.globals
return entry
},
closeSaveDialog() {
this.saveDialog = false
this.$emit('new-commit')
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,292 @@
<template>
<v-container>
<draggable
:list="entries"
class="dragArea pl-0"
tag="ul"
group="globals"
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
>
<div v-for="(entry, index) in entries" :key="entry.id">
<transition type="transition" :name="!drag ? 'flip-list' : null">
<div v-if="!entry.globals">
<div class="d-flex align-center">
<v-btn
v-if="remove"
class="entry-delete mr-5"
fab
rounded
x-small
color="error"
@click="emitRemoveAt(index)"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-text-field
ref="keyInput"
v-model="entry.key"
:rules="rules.keys(index, entries)"
:error-messages="
entry.valid
? null
: entry.key
? 'Each property name must be unique'
: 'This property needs a name!'
"
class="entry-key mr-5"
hint="property name"
filled
dense
rounded
/>
<v-text-field v-model="entry.value" class="entry-value mr-5" hint="property value" />
<v-btn
v-if="!remove"
v-tooltip="'Transform this field into an object'"
icon
small
@click="emitFieldToObject(entry, index)"
>
<v-icon color="primary">mdi-cube-outline</v-icon>
</v-btn>
</div>
</div>
<v-card v-else rounded="lg" class="pa-3 my-6" elevation="4">
<v-row align="center">
<v-col>
<v-card-title
v-if="!editTitle"
@mouseenter="mouseOver = true"
@mouseleave="mouseOver = false"
>
<v-btn
v-if="remove"
class="entry-delete mr-5"
fab
rounded
x-small
color="error"
@click="emitRemoveAt(index)"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
{{ entry.key }}
<v-btn v-if="mouseOver" icon small color="primary" @click="editTitle = true">
<v-icon small>mdi-pencil</v-icon>
</v-btn>
</v-card-title>
<v-card-title v-else>
<v-text-field
ref="keyInput"
v-model="entry.key"
:rules="rules.keys(index, entries)"
:error-messages="
entry.valid
? null
: entry.key
? 'Each property name must be unique'
: 'This property needs a name!'
"
></v-text-field>
<v-btn icon color="primary" @click="editTitle = false">
<v-icon small>mdi-check</v-icon>
</v-btn>
</v-card-title>
</v-col>
<v-col cols="auto">
<v-btn
v-tooltip="'Flatten this object into fields'"
class="mr-3"
icon
small
@click="emitObjectToField(entry, index)"
>
<v-icon color="primary">mdi-arrow-collapse-down</v-icon>
</v-btn>
</v-col>
</v-row>
<globals-entry
:entries="entry.globals"
:path="[...path, entry.id]"
:remove="remove"
v-on="$listeners"
/>
</v-card>
</transition>
</div>
</draggable>
<div
v-if="!remove"
slot="footer"
key="footer"
class="btn-group list-group-item mt-3"
role="group"
aria-label="Basic example"
>
<v-btn
v-tooltip="'Add a new field to this object'"
color="primary"
rounded
fab
small
@click="emitAddProp"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</v-container>
</template>
<script>
import draggable from 'vuedraggable'
import crs from 'crypto-random-string'
export default {
name: 'GlobalsEntry',
components: { draggable },
props: {
entries: {
type: Array,
default: null
},
path: {
type: Array,
default: null
},
streamId: {
type: String,
default: null
},
remove: {
type: Boolean,
default: false
},
invalidIds: {
type: Array,
default: () => []
}
},
data() {
return {
editTitle: false,
mouseOver: false,
drag: false,
valid: true,
rules: {
keys(index, entries) {
return [
(v) => {
let result = !!v || 'Properties need to have a name!'
entries[index].valid = result === true
return result
},
(v) => {
let filtered = entries.filter((_, i) => i != index)
let result =
filtered.findIndex((e) => e.key === v) === -1 || 'Each property name must be unique'
entries[index].valid = !!v && result === true
return result
}
]
}
},
errors: []
}
},
computed: {
dragOptions() {
return {
animation: 150,
disabled: false,
ghostClass: 'ghost'
}
}
},
methods: {
emitAddProp() {
var bimNouns = ['parameter', 'BIM', 'triple O', 'digital twin', 'LOD 9000', 'automation', 'structure', 'layer', 'interop']
var bimAdjs = ['parametric', 'chonky', '3D', 'liminal', 'brutalist', 'postmodern', 'discrete', 'dank']
var bimExclamations = ['wow', 'much', 'yes', 'towards a new']
var randomPhrase =
bimExclamations[Math.floor(Math.random() * bimExclamations.length)] +
' ' +
bimAdjs[Math.floor(Math.random() * bimAdjs.length)] +
' ' +
bimNouns[Math.floor(Math.random() * bimNouns.length)]
let field = {
key: `placeholder ${crs({ length: 6 })}`,
type: 'field',
value: randomPhrase,
valid: true,
id: crs({ length: 10 })
}
this.$emit('add-prop', { field: field, path: this.path })
},
emitRemoveAt(index) {
this.$emit('remove-prop', { path: this.path, index: index })
},
emitFieldToObject(entry, index) {
let obj = {
key: entry.key,
type: 'object',
id: entry.id,
valid: entry.valid,
globals: [
{
key: `placeholder ${crs({ length: 6 })}`,
type: 'field',
value: entry.value,
id: crs({ length: 10 }),
valid: true
}
]
}
this.$emit('field-to-object', { obj: obj, path: this.path, index: index })
},
emitObjectToField(entry, index) {
let fields = entry.globals
this.$emit('object-to-field', { fields: fields, path: this.path, index: index })
}
}
}
</script>
<style scoped>
.v-card {
background-color: rgba(0, 0, 0, 0.1);
}
.v-card__title {
font-weight: 500;
font-size: large;
letter-spacing: 1px;
text-transform: uppercase;
}
.v-text-field {
font-weight: 300;
}
.entry-key {
font-weight: 500;
position: relative;
top: 0.6rem;
}
.entry-delete {
position: relative;
top: -0.2rem;
}
.dragArea {
min-height: 50px;
}
.ghost {
opacity: 0.5;
}
.flip-list-move {
transition: transform 0.5s;
}
</style>
@@ -1,5 +1,5 @@
<template>
<v-list-item :to="`/streams/${streamId}/commits/${commit.id}`">
<v-list-item :to="route ? route : `/streams/${streamId}/commits/${commit.id}`">
<v-list-item-icon>
<user-avatar
:id="commit.authorId"
@@ -36,7 +36,7 @@ import SourceAppAvatar from './SourceAppAvatar'
export default {
components: { UserAvatar, SourceAppAvatar },
props: ['commit', 'streamId'],
props: ['commit', 'streamId', 'route'],
computed: {
commitDate() {
if (!this.commit) return null
@@ -30,7 +30,11 @@
small
color="primary"
class="pa-2"
:to="`/streams/${stream.id}/commits/${stream.commits.items[0].id}`"
:to="
stream.commits.items[0].branchName.startsWith('globals')
? `/streams/${stream.id}/${stream.commits.items[0].branchName}/${stream.commits.items[0].id}`
: `/streams/${stream.id}/commits/${stream.commits.items[0].id}`
"
>
<v-icon small class="mr-1">mdi-source-commit</v-icon>
{{ stream.commits.items[0].id }}
@@ -38,7 +42,11 @@
on
<router-link
class="text-decoration-none"
:to="`/streams/${stream.id}/branches/${stream.commits.items[0].branchName}`"
:to="
stream.commits.items[0].branchName.startsWith('globals')
? `/streams/${stream.id}/${stream.commits.items[0].branchName}`
: `/streams/${stream.id}/branches/${stream.commits.items[0].branchName}`
"
>
<v-icon small color="primary">mdi-source-branch</v-icon>
{{ stream.commits.items[0].branchName }}
@@ -93,6 +101,7 @@
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
Edit
</v-btn>
<v-dialog v-model="editStreamDialog" max-width="500">
<stream-edit-dialog
:stream-id="stream.id"
@@ -104,6 +113,18 @@
</v-dialog>
</v-card-text>
<v-card-text v-show="isHomeRoute">
<v-btn
v-tooltip="'Edit stream global variables!'"
block
small
elevation="0"
:to="`/streams/${stream.id}/globals`"
>
Globals
</v-btn>
</v-card-text>
<v-card-title v-show="isHomeRoute"><h5>Collaborators</h5></v-card-title>
<v-card-text v-show="isHomeRoute">
<v-row no-gutters>
@@ -2,12 +2,22 @@
<div style="display: inline-block">
<v-menu v-if="loggedIn" offset-x open-on-hover>
<template #activator="{ on, attrs }">
<v-avatar class="ma-1" color="grey lighten-3" :size="size" v-bind="attrs" v-on="on">
<v-avatar
v-if="user"
class="ma-1"
color="grey lighten-3"
:size="size"
v-bind="attrs"
v-on="on"
>
<v-img v-if="avatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
</v-avatar>
<v-avatar v-else class="ma-1" :size="size" v-bind="attrs" v-on="on">
<v-img contain src="/logo.svg"></v-img>
</v-avatar>
</template>
<v-card style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
<v-card v-if="user" style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
<v-card-text v-if="!$apollo.loading" class="text-center">
<v-avatar class="my-4" color="grey lighten-3" :size="40">
<v-img v-if="avatar" :src="avatar" />
@@ -24,6 +34,13 @@
<br />
</v-card-text>
</v-card>
<v-card v-else>
<v-card-text class="text-xs">
<b>Speckle Ghost</b>
<br />
This user no longer exists.
</v-card-text>
</v-card>
</v-menu>
<v-avatar v-else class="ma-1" color="grey lighten-3" :size="size">
<v-img v-if="avatar" :src="avatar" />
@@ -1,74 +1,64 @@
<template>
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<div v-if="branch.name !== 'main'">
<v-card-title>Edit Branch</v-card-title>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="updateBranch">
<v-card-text>
<v-text-field
v-model="name"
label="Name"
:rules="nameRules"
validate-on-blur
required
autofocus
></v-text-field>
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
<v-btn color="primary" block :disabled="!valid" type="submit">Save</v-btn>
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
<v-card :loading="loading" class="pa-4">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<div v-if="branch && branch.name !== 'main'">
<v-card-title>Edit Branch</v-card-title>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
<v-card-text>
<v-text-field
v-model="branch.name"
label="Name"
:rules="nameRules"
validate-on-blur
required
autofocus
></v-text-field>
<v-textarea v-model="branch.description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="cancel">Cancel</v-btn>
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
</v-card-actions>
</v-form>
<v-card-actions class="error--text body-2 pa-2">
<v-btn block x-small text color="error" @click="showDelete = true">Delete Branch</v-btn>
<v-dialog v-model="showDelete" max-width="500">
<v-card>
<v-card-title>Are you sure?</v-card-title>
<v-card-text>
You cannot undo this action. The branch
<b>{{ branch.name }}</b>
will be permanently deleted.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showDelete = false">Cancel</v-btn>
<v-btn color="error" text @click="deleteBranch">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-actions>
</v-form>
<v-card-actions class="error--text body-2 pa-2">
<v-btn block x-small text color="error" @click="showDelete = true">Delete Branch</v-btn>
<v-dialog v-model="showDelete" max-width="500">
<v-card>
<v-card-title>Are you sure?</v-card-title>
<v-card-text>
You cannot undo this action. The branch
<b>{{ name }}</b>
will be permanently deleted.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showDelete = false">Cancel</v-btn>
<v-btn color="error" text @click="deleteBranch">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-actions>
</div>
<div v-else>
<v-card-text>You cannot edit the main branch.</v-card-text>
</div>
</v-card>
</div>
<div v-else>
<v-card-text>You cannot edit the main branch.</v-card-text>
</div>
</v-card>
</v-dialog>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
streamId: {
type: String,
default: null
},
branch: {
type: Object,
default() {
return {
name: null,
description: null
}
}
}
},
data() {
return {
dialog: false,
branch: null,
valid: true,
loading: false,
name: this.branch.name,
showDelete: false,
nameRules: [
(v) => !!v || 'Branches need a name too!',
@@ -78,7 +68,6 @@ export default {
(v) => (v && v.length <= 100) || 'Name must be less than 100 characters',
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
],
description: this.branch.description,
isEdit: false,
pendingDelete: false,
allBranchNames: []
@@ -104,13 +93,26 @@ export default {
}
},
update(data) {
return data.stream.branches.items
.map((b) => b.name)
.filter((name) => name !== this.branch.name)
return data.stream.branches.items.filter((b) => b.name !== this.branch.name)
},
skip() {
return this.branch == null
}
}
},
computed: {
show: {
get() {
return this.dialog
},
set(value) {
this.dialog = value
if (value === false) {
this.cancel()
}
}
}
},
computed: {},
methods: {
async deleteBranch() {
this.loading = true
@@ -132,10 +134,27 @@ export default {
} catch (e) {
console.log(e)
}
this.$emit('close', { deleted: true })
this.loading = false
this.resolve({
result: true,
deleted: true
})
this.dialog = false
},
async updateBranch() {
open(branch) {
this.dialog = true
if (this.$refs.form) this.$refs.form.resetValidation()
this.branch = { ...branch }
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
},
async agree() {
if (!this.$refs.form.validate()) return
this.loading = true
@@ -150,13 +169,25 @@ export default {
params: {
streamId: this.$route.params.streamId,
id: this.branch.id,
name: this.name,
description: this.description
name: this.branch.name,
description: this.branch.description
}
}
})
this.loading = false
this.$emit('close', { name: this.name })
this.resolve({
result: true,
name: this.branch.name
})
this.dialog = false
},
cancel() {
this.resolve({
result: false
})
this.dialog = false
}
}
}
@@ -1,49 +1,47 @@
<template>
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>New Branch</v-card-title>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="createBranch">
<v-card-text>
<v-text-field
v-model="name"
label="Name"
:rules="nameRules"
validate-on-blur
required
autofocus
></v-text-field>
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>New Branch</v-card-title>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
<v-card-text>
<v-text-field
v-model="name"
label="Name"
:rules="nameRules"
validate-on-blur
required
autofocus
></v-text-field>
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
streamId: {
type: String,
default: null
},
branchNames: {
type: Array,
default: () => []
}
},
data() {
return {
dialog: false,
streamId: null,
branchNames: [],
valid: false,
loading: false,
name: null,
nameRules: [
(v) => !!v || 'Branches need a name too!',
(v) =>
(v && !v.startsWith('globals')) ||
'Globals is a reserved branch name. Please choose a different name.',
(v) =>
(v && this.branchNames.findIndex((e) => e === v) === -1) ||
'A branch with this name already exists',
@@ -55,9 +53,33 @@ export default {
pendingDelete: false
}
},
computed: {},
computed: {
show: {
get() {
return this.dialog
},
set(value) {
this.dialog = value
if (value === false) {
this.cancel()
}
}
}
},
methods: {
async createBranch() {
open(streamId, branchNames) {
this.dialog = true
if (this.$refs.form) this.$refs.form.resetValidation()
this.branchNames = branchNames
this.streamId = streamId
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
},
async agree() {
if (!this.$refs.form.validate()) return
this.loading = true
@@ -77,8 +99,19 @@ export default {
}
})
this.loading = false
this.$emit('close')
this.resolve({
result: true,
name: this.name
})
this.dialog = false
}
},
cancel() {
this.resolve({
result: false
})
this.dialog = false
}
}
</script>
@@ -0,0 +1,94 @@
<template>
<v-card :loading="loading">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>Save Globals</v-card-title>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="saveGlobals">
<v-card-text>
<v-text-field
v-model="message"
label="Message"
:rules="nameRules"
validate-on-blur
required
autofocus
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
streamId: {
type: String,
default: null
},
commitObj: {
type: Object,
default: null
},
branchName: {
type: String,
default: null
}
},
data() {
return {
valid: false,
loading: false,
name: null,
nameRules: [(v) => (v && v.length >= 3) || 'Message must be at least 3 characters'],
message: null
}
},
computed: {},
methods: {
async saveGlobals() {
if (!this.$refs.form.validate()) return
this.loading = true
this.$matomo && this.$matomo.trackPageView('globals/save')
let res = await this.$apollo.mutate({
mutation: gql`
mutation ObjectCreate($params: ObjectCreateInput!) {
objectCreate(objectInput: $params)
}
`,
variables: {
params: {
streamId: this.streamId,
objects: [this.commitObj]
}
}
})
await this.$apollo.mutate({
mutation: gql`
mutation CommitCreate($commit: CommitCreateInput!) {
commitCreate(commit: $commit)
}
`,
variables: {
commit: {
streamId: this.streamId,
branchName: this.branchName,
objectId: res.data.objectCreate[0],
message: this.message,
sourceApplication: 'web'
}
}
})
this.loading = false
this.$emit('close')
}
}
}
</script>
@@ -20,7 +20,11 @@
:rules="validation.emailRules"
label="email"
></v-text-field>
<v-text-field v-model="message" label="message"></v-text-field>
<v-text-field
v-model="message"
:rules="validation.messageRules"
label="message"
></v-text-field>
<v-card-actions>
<v-btn block color="primary" type="submit">Send invite</v-btn>
</v-card-actions>
@@ -32,6 +36,7 @@
</template>
<script>
import gql from 'graphql-tag'
import DOMPurify from 'dompurify'
export default {
name: 'ServerInviteDialog',
@@ -48,6 +53,17 @@ export default {
emailRules: [
(v) => !!v || 'E-mail is required',
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
],
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
}
]
}
}
@@ -21,7 +21,11 @@
:rules="validation.emailRules"
label="email"
></v-text-field>
<v-text-field v-model="message" label="message"></v-text-field>
<v-text-field
v-model="message"
:rules="validation.messageRules"
label="message"
></v-text-field>
<v-card-actions>
<v-btn block color="primary" type="submit">Send invite</v-btn>
</v-card-actions>
@@ -33,6 +37,7 @@
</template>
<script>
import gql from 'graphql-tag'
import DOMPurify from 'dompurify'
export default {
name: 'StreamInviteDialog',
@@ -55,6 +60,17 @@ export default {
emailRules: [
(v) => !!v || 'E-mail is required',
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
],
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
}
]
}
}
@@ -24,6 +24,7 @@ query Streams($cursor: String) {
createdAt
message
authorId
branchName
authorName
authorAvatar
referencedObject
+17 -1
View File
@@ -83,9 +83,25 @@ const routes = [
},
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')
},
{
path: 'branches/',
name: 'branchs',
name: 'branches',
meta: {
title: 'Branches | Speckle'
},
+21 -19
View File
@@ -5,13 +5,8 @@
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-card>
<v-card v-else rounded="lg" class="pa-4 mb-4" elevation="0">
<v-dialog v-model="dialogBranch" max-width="500">
<new-branch-dialog
:branch-names="branches.items.map((b) => b.name)"
:stream-id="$route.params.streamId"
@close="closeBranchDialog"
/>
</v-dialog>
<branch-new-dialog ref="newBranchDialog" />
<v-card-title>
<v-icon class="mr-2">mdi-source-branch</v-icon>
Branches
@@ -24,7 +19,7 @@
text
class="px-0"
small
@click="dialogBranch = true"
@click="newBranch"
>
<v-icon small class="mr-2 float-left">mdi-plus-circle-outline</v-icon>
New branch
@@ -40,10 +35,10 @@
</v-card>
<v-card v-if="!$apollo.queries.stream.loading" class="mt-5 pa-4" elevation="0" rounded="lg">
<v-subheader class="text-uppercase">Branches ({{ branches.items.length }})</v-subheader>
<v-subheader class="text-uppercase">Branches ({{ branches.length }})</v-subheader>
<v-card-text>
<v-list two-line color="transparent">
<template v-for="item in branches.items">
<template v-for="item in branches">
<v-list-item
:key="item.id"
:to="`/streams/${$route.params.streamId}/branches/${encodeURIComponent(item.name)}`"
@@ -71,14 +66,14 @@
</v-row>
</template>
<script>
import NewBranchDialog from '../components/dialogs/BranchNewDialog'
import BranchNewDialog from '../components/dialogs/BranchNewDialog'
import streamBranchesQuery from '../graphql/streamBranches.gql'
import gql from 'graphql-tag'
export default {
name: 'StreamMain',
components: {
NewBranchDialog
BranchNewDialog
},
props: {
userRole: {
@@ -87,9 +82,7 @@ export default {
}
},
data() {
return {
dialogBranch: false
}
return {}
},
apollo: {
stream: {
@@ -144,7 +137,7 @@ export default {
},
computed: {
branches() {
return this.stream.branches
return this.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
},
breadcrumbs() {
return [
@@ -170,9 +163,18 @@ export default {
this.$apollo.queries.stream.refetch()
},
methods: {
closeBranchDialog() {
this.dialogBranch = false
this.$apollo.queries.stream.refetch()
newBranch() {
this.$refs.newBranchDialog
.open(
this.$route.params.streamId,
this.branches.map((b) => b.name)
)
.then((dialog) => {
if (!dialog.result) return
else {
this.$apollo.queries.stream.refetch()
}
})
}
}
}
+18 -17
View File
@@ -6,9 +6,8 @@
</v-col>
<v-col v-else-if="stream.branch" cols="12">
<v-card class="pa-4" elevation="0" rounded="lg">
<v-dialog v-model="dialogEdit" max-width="500">
<branch-edit-dialog :branch="stream.branch" @close="closeEdit" />
</v-dialog>
<branch-edit-dialog ref="editBranchDialog" />
<v-card-title class="mr-8">
<v-icon class="mr-2">mdi-source-branch</v-icon>
<span class="d-inline-block">{{ stream.branch.name }}</span>
@@ -20,7 +19,7 @@
color="primary"
text
class="px-0"
@click="dialogEdit = true"
@click="editBranch"
>
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
Edit branch
@@ -144,19 +143,21 @@ export default {
}
},
methods: {
closeEdit({ name, deleted }) {
this.dialogEdit = false
if (deleted) {
this.$router.push({ path: `/streams/${this.streamId}` })
return
}
if (name !== this.$route.params.branchName) {
this.$router.push({
path: `/streams/${this.streamId}/branches/${encodeURIComponent(name)}/commits`
})
return
}
this.$apollo.queries.stream.refetch()
editBranch() {
this.$refs.editBranchDialog.open(this.stream.branch).then((dialog) => {
if (!dialog.result) return
else if (dialog.deleted) {
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)}/commits`
})
} else {
this.$apollo.queries.stream.refetch()
}
})
}
}
}
+122
View File
@@ -0,0 +1,122 @@
<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 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-actions>
<v-spacer />
<v-btn 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>
+147 -13
View File
@@ -9,7 +9,7 @@
<v-select
v-if="branches"
v-model="selectedBranch"
:items="branches.items"
:items="branches"
item-value="name"
solo
flat
@@ -41,8 +41,84 @@
:to="'/streams/' + $route.params.streamId + '/branches'"
>
<v-icon class="mr-2 float-left">mdi-source-branch</v-icon>
{{ branches.totalCount }} branch{{ branches.totalCount > 1 ? 'es' : '' }}
{{ branches.length }} branch{{ branches.length > 1 ? 'es' : '' }}
</v-btn>
<!-- DIALOGS -->
<branch-new-dialog ref="newBranchDialog" />
<branch-edit-dialog ref="editBranchDialog" />
<!-- MENU -->
<v-menu
v-if="userRole === 'contributor' || userRole === 'owner'"
offset-y
class="mx-2 mb-5"
>
<template #activator="{ on, attrs }">
<v-btn
style="position: absolute; top: 36px; right: 20px"
color="primary"
v-bind="attrs"
icon
v-on="on"
>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="newBranch">
<v-list-item-action class="mr-2">
<v-icon>mdi-plus-circle-outline</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>New branch</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="selectedBranch && selectedBranch.name != 'main'"
@click="editBranch"
>
<v-list-item-action class="mr-2">
<v-icon>mdi-cog-outline</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>Edit {{ selectedBranch.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<div
v-if="
stream &&
stream.commit &&
stream.commit.branchName != 'main' &&
stream.commit.branchName != selectedBranch.name
"
class="pb-2 caption"
>
<v-alert color="primary" class="caption" dense text type="info">
The last commit of this stream is on the
<v-btn
text
x-small
color="primary darken-1"
:to="'/streams/' + $route.params.streamId + '/branches/' + stream.commit.branchName"
>
{{ stream.commit.branchName }}
</v-btn>
branch, see
<v-btn
x-small
text
color="primary darken-1"
:to="'/streams/' + $route.params.streamId + '/commits/' + stream.commit.id"
>
{{ stream.commit.message }}
</v-btn>
</v-alert>
</div>
</v-sheet>
<div v-if="latestCommit" style="height: 50vh">
@@ -152,7 +228,7 @@
/>
</v-dialog>
<v-card-title>
Description
Stream Description
<v-spacer />
<v-btn
v-if="userRole === 'owner'"
@@ -193,6 +269,8 @@ 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',
@@ -202,7 +280,9 @@ export default {
SourceAppAvatar,
NoDataPlaceholder,
Renderer,
ErrorBlock
ErrorBlock,
BranchNewDialog,
BranchEditDialog
},
props: {
userRole: {
@@ -213,10 +293,11 @@ export default {
data() {
return {
dialogDescription: false,
dialogBranch: false,
selectedBranch: null,
clearRendererTrigger: 0,
error: ''
error: '',
dialogBranchNew: false,
dialogBranchEdit: false
}
},
apollo: {
@@ -228,7 +309,7 @@ export default {
}
},
update(data) {
return data.stream.branches
return data.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
}
},
description: {
@@ -247,6 +328,26 @@ export default {
},
update: (data) => data.stream.description
},
stream: {
query: gql`
query($id: String!) {
stream(id: $id) {
id
commit {
branchName
id
message
}
}
}
`,
variables() {
return {
id: this.$route.params.streamId
}
}
//update: (data) => data.stream.description
},
$subscribe: {
branchCreated: {
query: gql`
@@ -297,7 +398,7 @@ export default {
},
branchNames() {
if (!this.branches) return []
return this.branches.items.map((b) => b.name)
return this.branches.map((b) => b.name)
},
compiledStreamDescription() {
if (!this.description) return ''
@@ -337,15 +438,48 @@ export default {
this.dialogDescription = false
this.$apollo.queries.description.refetch()
},
closeBranchDialog() {
this.dialogBranch = false
this.$apollo.queries.branches.refetch()
editBranch() {
this.$refs.editBranchDialog.open(this.selectedBranch).then((dialog) => {
if (!dialog.result) return
else if (dialog.deleted) {
this.$router.push({ path: `/streams/${this.$route.params.streamId}` })
} else if (dialog.name !== this.selectedBranch.name) {
//this.$router.push does not work, refresh entire window
window.location =
window.origin +
'/streams/' +
this.$route.params.streamId +
'/branches/' +
encodeURIComponent(dialog.name)
} else {
this.$apollo.queries.branches.refetch()
}
})
},
newBranch() {
this.$refs.newBranchDialog
.open(
this.$route.params.streamId,
this.branches.map((b) => b.name)
)
.then((dialog) => {
if (!dialog.result) return
else {
//this.$router.push does not work, refresh entire window
window.location =
window.origin +
'/streams/' +
this.$route.params.streamId +
'/branches/' +
encodeURIComponent(dialog.name)
}
})
},
selectBranch() {
if (!this.branches) return
let branchName = this.$route.params.branchName ? this.$route.params.branchName : 'main'
let index = this.branches.items.findIndex((x) => x.name === branchName)
if (index > -1) this.selectedBranch = this.branches.items[index]
let index = this.branches.findIndex((x) => x.name === branchName)
if (index > -1) this.selectedBranch = this.branches[index]
else this.error = 'Branch ' + branchName + ' does not exist'
},
changeBranch() {
+7 -1
View File
@@ -39,7 +39,13 @@
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="subtitle-2">
<router-link :to="'streams/' + a.streamId + '/commits/' + a.id">
<router-link
:to="
a.branchName.startsWith('globals')
? `streams/${a.streamId}/${a.branchName}/${a.id}`
: `streams/${a.streamId}/commits/${a.id}`
"
>
{{ a.message }}
</router-link>
</v-list-item-title>
@@ -40,7 +40,9 @@ module.exports = {
Branch: {
async author( parent, args, context, info ) {
return await getUserById( { userId: parent.authorId } )
if ( parent.userId )
return await getUserById( { userId: parent.authorId } )
else return null
}
},
@@ -40,8 +40,8 @@ module.exports = {
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let {cursor, users} = await searchUsers( args.query, args.limit, args.cursor )
return {cursor: cursor, items: users}
let { cursor, users } = await searchUsers( args.query, args.limit, args.cursor )
return { cursor: cursor, items: users }
},
async userPwdStrength( parent, args, context, info ) {
@@ -12,7 +12,7 @@ extend type User {
type Branch {
id: String!
name: String!
author: User!
author: User
description: String
commits(limit: Int! = 25, cursor: String): CommitCollection
}
@@ -0,0 +1,44 @@
// /* istanbul ignore file */
exports.up = async ( knex ) => {
await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" DROP NOT NULL;' )
await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' )
await knex.raw( `
ALTER TABLE commits
ADD CONSTRAINT commits_author_foreign
FOREIGN KEY (author)
REFERENCES users(id)
ON DELETE SET NULL;
` )
await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' )
await knex.raw( `
ALTER TABLE branches
ADD CONSTRAINT branches_authorid_foreign
FOREIGN KEY ("authorId")
REFERENCES users(id)
ON DELETE SET NULL;
` )
}
exports.down = async ( knex ) => {
// NOTE:
// This migration cannot run backwards: if a user deletes their account, the previous not null
// constraint cannot be satisfied. Therefore, there's no going back (and there isn't really a need either).
// await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' )
// await knex.raw( `
// ALTER TABLE branches
// ADD CONSTRAINT branches_authorid_foreign
// FOREIGN KEY ("authorId")
// REFERENCES users(id);
// ` )
// await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' )
// await knex.raw( `
// ALTER TABLE commits
// ADD CONSTRAINT commits_author_foreign
// FOREIGN KEY (author)
// REFERENCES users(id);
// ` )
// await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" SET NOT NULL;' )
}
@@ -8,9 +8,11 @@ const cors = require( 'cors' )
const { matomoMiddleware } = require( `${appRoot}/logging/matomoHelper` )
const { contextMiddleware, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
const { validatePermissionsReadStream } = require( './authUtils' )
const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
const { getObjectsStream } = require( '../services/objects' )
const { pipeline } = require( 'stream' )
module.exports = ( app ) => {
app.options( '/api/getobjects/:streamId', cors() )
@@ -24,72 +26,25 @@ module.exports = ( app ) => {
let simpleText = req.headers.accept === 'text/plain'
let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } )
let currentChunkSize = 0
let maxChunkSize = 50000
let chunk = simpleText ? '' : [ ]
let isFirstBuffer = true
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } )
const gzip = zlib.createGzip( )
let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } )
let speckleObjStream = new SpeckleObjectsStream( simpleText )
let gzipStream = zlib.createGzip( )
if ( !simpleText ) gzip.write( '[' )
// helper func to flush the gzip buffer
const writeBuffer = () => {
if ( simpleText ) {
gzip.write( chunk )
} else {
if ( !isFirstBuffer ) {
gzip.write( ',' )
}
gzip.write( chunk.join( ',' ) )
}
gzip.flush( )
chunk = simpleText ? '' : [ ]
isFirstBuffer = false
}
let k = 0
let requestDropped = false
dbStream.on( 'data', row => {
try {
let data = JSON.stringify( row.data )
currentChunkSize += Buffer.byteLength( data, 'utf8' )
if ( simpleText ) {
chunk += `${row.data.id}\t${data}\n`
pipeline(
dbStream,
speckleObjStream,
gzipStream,
res,
( err ) => {
if ( err ) {
debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error streaming objects from stream ${req.params.streamId}: ${err}` )
} else {
chunk.push( data )
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Streamed ${childrenList.length} objects from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` )
}
if ( currentChunkSize >= maxChunkSize ) {
currentChunkSize = 0
writeBuffer()
}
k++
} catch ( e ) {
requestDropped = true
debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` )
return
}
} )
)
dbStream.on( 'error', err => {
debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` )
requestDropped = true
return
} )
dbStream.on( 'end', ( ) => {
if ( currentChunkSize !== 0 ) {
writeBuffer()
}
if ( !simpleText ) gzip.write( ']' )
gzip.end( )
} )
// 🚬
gzip.pipe( res )
} )
}
@@ -19,6 +19,8 @@ module.exports = ( app ) => {
let objectList = JSON.parse( req.body.objects )
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Diffing ${objectList.length} objects for stream ${req.params.streamId}` )
let response = await hasObjects( { streamId: req.params.streamId, objectIds: objectList } )
// console.log(response)
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' } )
+18 -69
View File
@@ -10,6 +10,8 @@ const { contextMiddleware } = require( `${appRoot}/modules/shared` )
const { validatePermissionsReadStream } = require( './authUtils' )
const { getObject, getObjectChildrenStream } = require( '../services/objects' )
const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
const { pipeline } = require( 'stream' )
module.exports = ( app ) => {
@@ -28,85 +30,30 @@ module.exports = ( app ) => {
return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` )
}
obj = obj.data
let simpleText = req.headers.accept === 'text/plain'
let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } )
let currentChunkSize = 0
let maxChunkSize = 50000
let chunk = simpleText ? '' : [ ]
let isFirst = true
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } )
const gzip = zlib.createGzip( )
let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } )
let speckleObjStream = new SpeckleObjectsStream( simpleText )
let gzipStream = zlib.createGzip( )
if ( !simpleText ) gzip.write( '[' )
speckleObjStream.write( obj )
// helper func to flush the gzip buffer
const writeBuffer = ( addStartingComma ) => {
if ( simpleText ) {
gzip.write( chunk )
} else {
if ( addStartingComma ) {
gzip.write( ',' )
}
gzip.write( chunk.join( ',' ) )
}
gzip.flush( )
chunk = simpleText ? '' : [ ]
}
var objString = JSON.stringify( obj )
if ( simpleText ) {
chunk += `${obj.id}\t${objString}\n`
} else {
chunk.push( objString )
}
writeBuffer( false )
let k = 0
let requestDropped = false
dbStream.on( 'data', row => {
try {
let data = JSON.stringify( row.data )
currentChunkSize += Buffer.byteLength( data, 'utf8' )
if ( simpleText ) {
chunk += `${row.data.id}\t${data}\n`
pipeline(
dbStream,
speckleObjStream,
gzipStream,
res,
( err ) => {
if ( err ) {
debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error downloading object ${req.params.objectId} from stream ${req.params.streamId}: ${err}` )
} else {
chunk.push( data )
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded object ${req.params.objectId} from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` )
}
if ( currentChunkSize >= maxChunkSize ) {
currentChunkSize = 0
writeBuffer( true )
}
k++
} catch ( e ) {
requestDropped = true
debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` )
return
}
} )
)
dbStream.on( 'error', err => {
debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` )
requestDropped = true
return
} )
dbStream.on( 'end', ( ) => {
if ( currentChunkSize !== 0 ) {
writeBuffer( true )
}
if ( !simpleText ) gzip.write( ']' )
gzip.end( )
} )
// 🚬
gzip.pipe( res )
} )
app.options( '/objects/:streamId/:objectId/single', cors() )
@@ -122,6 +69,8 @@ module.exports = ( app ) => {
return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` )
}
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded single object ${req.params.objectId} from stream ${req.params.streamId}` )
res.send( obj.data )
} )
}
@@ -0,0 +1,37 @@
const { Transform } = require( 'stream' )
// A stream that converts database objects stream to "{id}\t{data_json}\n" stream or a json stream of obj.data fields
class SpeckleObjectsStream extends Transform {
constructor( simpleText ) {
super( { writableObjectMode: true } )
this.simpleText = simpleText
if ( !this.simpleText ) this.push( '[' )
this.isFirstObject = true
}
_transform( dbObj, encoding, callback ) {
try {
if ( this.simpleText ) {
this.push( `${dbObj.data.id}\t${JSON.stringify( dbObj.data )}\n` )
} else {
// JSON output
if ( !this.isFirstObject ) this.push( ',' )
this.push( JSON.stringify( dbObj.data ) )
this.isFirstObject = false
}
callback()
} catch ( e ) {
callback( e )
}
}
_flush( callback ) {
if ( !this.simpleText ) this.push( ']' )
callback()
}
}
exports.SpeckleObjectsStream = SpeckleObjectsStream
@@ -18,8 +18,6 @@ module.exports = ( app ) => {
return res.status( hasStreamAccess.status ).end()
}
debug( 'speckle:upload-endpoint' )( 'Upload started' )
let busboy = new Busboy( { headers: req.headers } )
let totalProcessed = 0
let last = {}
@@ -94,7 +92,6 @@ module.exports = ( app ) => {
await Promise.all( promises )
debug( 'speckle:upload-endpoint' )( 'Upload ended' )
res.status( 201 ).end( )
} )
@@ -63,9 +63,9 @@ module.exports = {
let query = await Commits( )
.columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
.select( )
.join( 'users', 'commits.author', 'users.id' )
.join( 'branch_commits', 'commits.id', 'branch_commits.commitId' )
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
.leftJoin( 'users', 'commits.author', 'users.id' )
.where( { 'commits.id': id } )
.first( )
return await query
@@ -97,8 +97,8 @@ module.exports = {
.columns( [ { id: 'commitId' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' },{ authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
.select( )
.join( 'commits', 'commits.id', 'branch_commits.commitId' )
.join( 'users', 'commits.author', 'users.id' )
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
.leftJoin( 'users', 'commits.author', 'users.id' )
.where( 'branchId', branchId )
if ( cursor )
@@ -132,9 +132,9 @@ module.exports = {
.columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
.select( )
.join( 'commits', 'commits.id', 'stream_commits.commitId' )
.join( 'users', 'commits.author', 'users.id' )
.join( 'branch_commits', 'commits.id', 'branch_commits.commitId' )
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
.leftJoin( 'users', 'commits.author', 'users.id' )
.where( 'stream_commits.streamId', streamId )
@@ -203,7 +203,7 @@ module.exports = {
} )
.where( knex.raw( 'object_children_closure."streamId" = ? AND parent = ?', [ streamId, objectId ] ) )
.orderBy( 'objects.id' )
return q.stream( )
return q.stream( { highWaterMark: 2 } )
},
async getObjectChildren( { streamId, objectId, limit, depth, select, cursor } ) {
@@ -442,7 +442,7 @@ module.exports = {
.andWhere( 'streamId', streamId )
.orderBy( 'id' )
.select( 'id', 'speckleType', 'totalChildrenCount', 'totalChildrenCountByDepth', 'createdAt', 'data' )
return res.stream( )
return res.stream( { highWaterMark: 2 } )
},
async hasObjects( { streamId, objectIds } ) {
@@ -63,14 +63,16 @@ module.exports = {
async getUserById( { userId } ) {
let user = await Users( ).where( { id: userId } ).select( '*' ).first( )
delete user.passwordDigest
if ( user )
delete user.passwordDigest
return user
},
// TODO: deprecate
async getUser( id ) {
let user = await Users( ).where( { id: id } ).select( '*' ).first( )
delete user.passwordDigest
if ( user )
delete user.passwordDigest
return user
},
@@ -150,8 +152,6 @@ module.exports = {
for ( let i in streams.rows ) {
await deleteStream( { streamId: streams.rows[i].id } )
}
await knex.raw( 'DELETE FROM commits WHERE author = ?', [ id ] )
await knex.raw( 'DELETE FROM branches WHERE "authorId" = ?', [ id ] )
return await Users( ).where( { id: id } ).del( )
}
@@ -15,6 +15,21 @@ const { createUser, findOrCreateUser, getUser, searchUsers, updateUser, deleteUs
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' )
const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' )
const {
createBranch,
getBranchesByStreamId
} = require( '../services/branches' )
const {
createCommitByBranchName,
getCommitsByBranchName,
getCommitById,
getCommitsByStreamId,
deleteCommit,
} = require( '../services/commits' )
const { createObject, createObjects } = require( '../services/objects' )
describe( 'Actors & Tokens @user-services', ( ) => {
let myTestActor = {
name: 'Dimitrie Stefanescu',
@@ -110,29 +125,53 @@ describe( 'Actors & Tokens @user-services', ( ) => {
} )
// Note: deletion is more complicated.
it( 'Should delete a user', async ( ) => {
let soloOwnerStream = { name: 'Test Stream 01', description: 'wonderful test stream', isPublic: true }
let multiOwnerStream = { name: 'Test Stream 02', description: 'another test stream', isPublic: true }
soloOwnerStream.id = await createStream( { ...soloOwnerStream, ownerId: ballmerUserId } )
multiOwnerStream.id = await createStream( { ...multiOwnerStream, ownerId: ballmerUserId } )
await grantPermissionsStream( { streamId: multiOwnerStream.id, userId: myTestActor.id, role: 'stream:owner' } )
// create a branch for ballmer on the multiowner stream
let branch = { name: 'ballmer/dev' }
branch.id = await createBranch( { ...branch, streamId: multiOwnerStream.id, authorId: ballmerUserId } )
let branchSecond = { name: 'steve/jobs' }
branchSecond.id = await createBranch( { ...branchSecond, streamId: multiOwnerStream.id, authorId: myTestActor.id } )
// create an object and a commit around it on the multiowner stream
let objId = await createObject( multiOwnerStream.id, { pie: 'in the sky' } )
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId:objId, authorId: ballmerUserId } )
await deleteUser( ballmerUserId )
if ( await getStream( { streamId: soloOwnerStream.id } ) !== undefined ) {
assert.fail( 'user stream not deleted' )
}
let multiOwnerStreamCopy = await getStream( { streamId: multiOwnerStream.id } )
if ( !multiOwnerStreamCopy || multiOwnerStreamCopy.id != multiOwnerStream.id ) {
assert.fail( 'shared stream deleted' )
}
try {
let user = await getUser( ballmerUserId )
} catch ( e ) {
return
}
assert.fail( 'user not deleted' )
let branches = await getBranchesByStreamId( { streamId: multiOwnerStream.id } )
expect( branches.items.length ).to.equal( 3 )
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName:'ballmer/dev' } )
expect( branchCommits.commits.length ).to.equal( 1 )
let commit = await getCommitById( { id: commitId } )
expect( commit ).to.be.not.null
let commitsByStreamId = await getCommitsByStreamId( { streamId: multiOwnerStream.id } )
expect( commitsByStreamId.commits.length ).to.equal( 1 )
let user = await getUser( ballmerUserId )
if ( user )
assert.fail( 'user not deleted' )
} )
it( 'Should get a user', async ( ) => {
@@ -2,6 +2,7 @@
const appRoot = require( 'app-root-path' )
const crs = require( 'crypto-random-string' )
const knex = require( `${appRoot}/db/knex` )
const sanitizeHtml = require( 'sanitize-html' )
const { getUserByEmail, getUserById } = require( `${appRoot}/modules/core/services/users` )
@@ -19,6 +20,15 @@ module.exports = {
if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' )
if ( message ) {
if ( message.length >= 1024 ) {
throw new Error( 'Personal message too long.' )
}
message = module.exports.sanitizeMessage( message )
}
// check if email is already invited
let existingInvite = await module.exports.getInviteByEmail( { email } )
if ( existingInvite ) throw new Error( 'Already invited!' )
@@ -126,5 +136,11 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
await Invites().where( { id: id } ).update( { used: true } )
return true
},
async sanitizeMessage( message ) {
return sanitizeHtml( message, {
allowedTags: [ 'b', 'i', 'em', 'strong' ],
} )
}
}
@@ -4,6 +4,7 @@ const chai = require( 'chai' )
const request = require( 'supertest' )
const assert = require( 'assert' )
const appRoot = require( 'app-root-path' )
const { async } = require( 'crypto-random-string' )
const { init, startHttp } = require( `${appRoot}/app` )
const expect = chai.expect
@@ -11,9 +12,8 @@ const expect = chai.expect
const knex = require( `${appRoot}/db/knex` )
const { createUser } = require( `${appRoot}/modules/core/services/users` )
const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite } = require( `${appRoot}/modules/serverinvites/services` )
const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite, sanitizeMessage } = require( `${appRoot}/modules/serverinvites/services` )
const { createStream, getStream, getStreamUsers, getUserStreams } = require( `${appRoot}/modules/core/services/streams` )
const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services/tokens` )
const serverAddress = `http://localhost:${process.env.PORT || 3000}`
@@ -55,30 +55,51 @@ describe( 'Server Invites @server-invites', ( ) => {
try {
await createAndSendInvite( { email:'cat@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
assert.fail()
} catch ( e ) {
// pass
return
}
assert.fail( 'should not allow multiple invites for the same email' )
} )
it( 'should not allow self invites', async() => {
try {
await createAndSendInvite( { email: 'didimitrie-100@gmail.com', inviterId: actor.id } )
assert.fail()
} catch ( e ) {
// pass
return
}
assert.fail( 'should not allow self invites' )
} )
it( 'should not allow invites from no user', async() => {
try {
await createAndSendInvite( { email: 'didimitrie233-100@gmail.com', inviterId: 'fake' } )
assert.fail()
} catch ( e ) {
// pass
return
}
assert.fail( 'should not allow invites from no user' )
} )
it( 'should not allow invites with a too long message', async() => {
try {
let inviteId = await createAndSendInvite( {
email: '123456@gmail.com',
inviterId: actor.id,
message: longInviteMessage
} )
} catch ( e ){
return
}
assert.fail( 'created invite with too long message' )
} )
it( 'should sanitize invite messages', async() => {
let clean = await sanitizeMessage( 'Click on my <b><a href="https://spam.com">spam link please</a></b>!' )
const includesLink = clean.includes( '<a' )
expect( includesLink ).to.be.false
} )
it( 'should get an invite by id', async() => {
@@ -220,3 +241,6 @@ function sendRequest( auth, obj, address = serverAddress ) {
return chai.request( address ).post( '/graphql' ).set( 'Authorization', auth ).send( obj )
}
const longInviteMessage =
'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.'
+117
View File
@@ -4498,6 +4498,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"default-require-extensions": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz",
@@ -4666,6 +4671,39 @@
"esutils": "^2.0.2"
}
},
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
},
"domhandler": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
"integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
"requires": {
"domelementtype": "^2.2.0"
}
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
},
"dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
@@ -4798,6 +4836,11 @@
"ansi-colors": "^4.1.1"
}
},
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"env-paths": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
@@ -6794,6 +6837,17 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
}
},
"http-cache-semantics": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
@@ -7834,6 +7888,11 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
},
"knex": {
"version": "0.21.15",
"resolved": "https://registry.npmjs.org/knex/-/knex-0.21.15.tgz",
@@ -8830,6 +8889,11 @@
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"optional": true
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw=="
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -9776,6 +9840,11 @@
"protocols": "^1.4.0"
}
},
"parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
},
"parse-url": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz",
@@ -10073,6 +10142,23 @@
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
},
"postcss": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
"integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==",
"requires": {
"colorette": "^1.2.2",
"nanoid": "^3.1.23",
"source-map-js": "^0.6.2"
},
"dependencies": {
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
}
}
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -10863,6 +10949,32 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-html": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz",
"integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==",
"requires": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^6.0.0",
"is-plain-object": "^5.0.0",
"klona": "^2.0.3",
"parse-srcset": "^1.0.2",
"postcss": "^8.0.2"
},
"dependencies": {
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
}
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -11245,6 +11357,11 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"source-map-js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
"integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug=="
},
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+1
View File
@@ -60,6 +60,7 @@
"pg-query-stream": "^3.4.2",
"prom-client": "^13.1.0",
"redis": "^3.1.1",
"sanitize-html": "^2.4.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
-6
View File
@@ -2008,12 +2008,6 @@
"@sinonjs/commons": "^1.7.0"
}
},
"@speckle/objectloader": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.0.3.tgz",
"integrity": "sha512-hSyJU0ktZOYbgjDtwrHHXowRu5L0lxoO32N2JPJUjURoy+M1ZpvJVGyT4jKG03HvH30j9rynja0PVjVoW9LdEw==",
"dev": true
},
"@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
+15
View File
@@ -91,6 +91,7 @@ export default class Coverter {
// Last attempt: iterate through all object keys and see if we can display anything!
// traverses the object in case there's any sub-objects we can convert.
for ( let prop in target ) {
if ( prop === 'bbox' ) continue
if ( typeof target[prop] !== 'object' ) continue
let childPromise = this.traverseAndConvert( target[prop], callback, scale )
childrenConversionPromisses.push( childPromise )
@@ -363,6 +364,20 @@ export default class Coverter {
return new ObjectWrapper( geometry, obj, 'line' )
}
async BoxToBufferGeometry( object, scale = true ){
let conversionFactor = scale ? getConversionFactor( object.units ) : 1
var move = this.PointToVector3( object.basePlane.origin )
var width = ( object.xSize.end - object.xSize.start ) * conversionFactor
var depth = ( object.ySize.end - object.ySize.start ) * conversionFactor
var height = ( object.zSize.end - object.zSize.start ) * conversionFactor
var box = new THREE.BoxBufferGeometry( width,height,depth,1,1,1 )
box.applyMatrix4( new THREE.Matrix4().setPosition( move ) )
return new ObjectWrapper( box, object )
}
async PolycurveToBufferGeometry( object, scale = true ) {
let obj = {}
Object.assign( obj, object )