Merge branch 'main' into andrew/fe2-expand-object-arrays

This commit is contained in:
andrewwallacespeckle 2024-03-07 10:27:58 +01:00
Родитель 3e6908ca52 5d7df71d28
Коммит 3eb81ffbd9
56 изменённых файлов: 1373 добавлений и 658 удалений

Просмотреть файл

@ -8,7 +8,7 @@ DOCKER_FILE_NAME="$(echo ${DOCKER_IMAGE_TAG}_${IMAGE_VERSION_TAG} | sed -e 's/[^
# shellcheck disable=SC2068,SC2046
LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)" # get the last release tag. FIXME: Fails if a commit is tagged with more than one tag: https://stackoverflow.com/questions/8089002/git-describe-with-two-tags-on-the-same-commit/56039163#56039163
# shellcheck disable=SC2034
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | python -c "parts = input().split('.'); parts[-1] = str(int(parts[-1])+1); print('.'.join(parts))")"
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')"
# shellcheck disable=SC2034
BRANCH_NAME_TRUNCATED="$(echo "${CIRCLE_BRANCH}" | cut -c -50 | sed 's/[^a-zA-Z0-9_.-]/_/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough
# shellcheck disable=SC2034

Просмотреть файл

@ -91,6 +91,10 @@ workflows:
- pre-commit:
filters: *filters-allow-all
- build-image-approval:
type: approval
filters: *filters-ignore-main-branch-or-all-tags
- docker-build-server:
context: &build-context
- github-readonly-public-repos
@ -99,54 +103,63 @@ workflows:
only: /.*/
requires:
- get-version
- build-image-approval
- docker-build-frontend:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-frontend-2:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-webhooks:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-file-imports:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-previews:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-test-container:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-monitor-container:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- docker-build-docker-compose-ingress:
context: *build-context
filters: *filters-build
requires:
- get-version
- build-image-approval
- publish-approval:
type: approval
@ -299,8 +312,8 @@ workflows:
jobs:
get-version:
docker:
- image: cimg/python:3.12.1
docker: &docker-base-image
- image: cimg/base:2024.02
working_directory: &work-dir /tmp/ci
steps:
- checkout
@ -338,8 +351,10 @@ jobs:
steps:
- checkout
- restore_cache:
name: Restore pre-commit & Yarn Package cache
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
@ -347,10 +362,6 @@ jobs:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn
@ -471,7 +482,7 @@ jobs:
# destination: package/server/coverage
test-frontend-2:
docker:
docker: &docker-node-browsers-image
- image: cimg/node:18.19.0-browsers
resource_class: xlarge
steps:
@ -505,7 +516,7 @@ jobs:
working_directory: 'packages/frontend-2'
test-dui-3:
docker:
docker: &docker-node-image
- image: cimg/node:18.19.0
resource_class: medium+
steps:
@ -535,8 +546,7 @@ jobs:
working_directory: 'packages/dui3'
test-ui-components:
docker:
- image: cimg/node:18.19.0-browsers
docker: *docker-node-browsers-image
resource_class: xlarge
steps:
- checkout
@ -589,8 +599,7 @@ jobs:
ui-components-chromatic:
resource_class: medium+
docker:
- image: cimg/node:18.19.0
docker: *docker-node-image
steps:
- checkout
- restore_cache:
@ -627,14 +636,13 @@ jobs:
# but it is not possible to scan npm/yarn package.json
# because it requires node_modules
# therefore this scanning has to be triggered via the cli
docker: &docker-image
- image: cimg/python:3.12.1-node
docker: *docker-node-image
resource_class: small
working_directory: *work-dir
steps:
- checkout
- restore_cache:
name: Restore Yarn Package Cache
name: Restore Yarn Package cache
keys:
- yarn-packages-server-{{ checksum "yarn.lock" }}
- run:
@ -668,6 +676,7 @@ jobs:
sudo mkdir /nix
sudo chmod 777 /nix
- restore_cache:
name: Restore nix cache
keys:
- nix-{{ checksum "./.circleci/deployment/docker-compose-shell.nix" }}
- run:
@ -701,6 +710,7 @@ jobs:
sudo mkdir /nix
sudo chmod 777 /nix
- restore_cache:
name: Restore nix cache
keys:
- nix-{{ checksum "./.circleci/deployment/helm-chart-shell.nix" }}
- run:
@ -761,28 +771,14 @@ jobs:
./.circleci/deployment/helm-chart-shell.nix
docker-build: &build-job
docker: &docker-image
- image: cimg/python:3.12.1-node
docker: *docker-base-image
resource_class: medium
working_directory: *work-dir
steps:
- checkout
- attach_workspace:
at: /tmp/ci/workspace
- run:
name: determine if draft PR
command: |
echo "export IS_DRAFT_PR=$(.circleci/is_draft.sh)" >> workspace/env-vars
- run: cat workspace/env-vars >> $BASH_ENV
- run: echo "IS_DRAFT_PR=${IS_DRAFT_PR}"
- run:
name: 'Check if should proceed'
command: |
[[ "${CIRCLE_TAG}" ]] && echo "proceed because tag is set" && exit 0
[[ "${CIRCLE_BRANCH}" == "main" ]] && echo "proceed because main branch" && exit 0
[[ "${CIRCLE_BRANCH}" == "testing" ]] && echo "proceed because testing branch" && exit 0
[[ "${IS_DRAFT_PR}" == "TRUE" || -z "${CIRCLE_PULL_REQUEST}" ]] && echo "Should not build because either Draft PR or branch without PR, stopping" && exit 1
echo "proceeding"
- setup_remote_docker:
version: default
docker_layer_caching: true
@ -843,8 +839,7 @@ jobs:
SPECKLE_SERVER_PACKAGE: docker-compose-ingress
docker-publish: &publish-job
docker:
- image: cimg/python:3.12.1-node
docker: *docker-base-image
resource_class: medium
working_directory: *work-dir
steps:
@ -908,14 +903,13 @@ jobs:
SPECKLE_SERVER_PACKAGE: docker-compose-ingress
publish-npm:
docker: *docker-image
docker: *docker-node-image
working_directory: *work-dir
steps:
- checkout
- attach_workspace:
at: /tmp/ci/workspace
- run: cat workspace/env-vars >> $BASH_ENV
- restore_cache:
name: Restore Yarn Package Cache
keys:
@ -932,7 +926,6 @@ jobs:
paths:
- .yarn/cache
- .yarn/unplugged
- run:
name: auth to npm as Speckle
command: |
@ -955,7 +948,8 @@ jobs:
command: 'yarn workspaces foreach -pv --no-private npm publish --access public'
publish-helm-chart:
docker: *docker-image
docker:
- image: cimg/python:3.12.1
working_directory: *work-dir
steps:
- checkout
@ -970,7 +964,7 @@ jobs:
command: ./.circleci/publish_helm_chart.sh
update-helm-documentation:
docker: *docker-image
docker: *docker-node-image
working_directory: *work-dir
steps:
- checkout

Просмотреть файл

@ -18,9 +18,11 @@ def docker_load(name, filename=None, existing_ref=None, deps=None):
docker tag "{EXISTING_REF}" "$EXPECTED_REF"'.format(DOCKER_FILE_NAME=filename, EXISTING_REF=existing_ref),
deps=deps)
def speckle_image(package):
def speckle_image(package,original_package_name=None):
if not package:
fail('package must be specified')
if not original_package_name:
original_package_name = package
image_version_tag = os.getenv('IMAGE_VERSION_TAG')
if not image_version_tag:
@ -31,7 +33,8 @@ def speckle_image(package):
workspace='/tmp/ci/workspace'
docker_image_tag = 'speckle/speckle-{}'.format(package)
existing_ref = '{}:{}'.format(docker_image_tag, image_version_tag)
original_docker_image_tag = 'speckle/speckle-{}'.format(original_package_name)
existing_ref = '{}:{}'.format(original_docker_image_tag, image_version_tag)
docker_file_name = "".join([ c if c.isalnum() or c=='-' or c=='_' or c=='.' else "_" for c in existing_ref.elems() ])
return docker_load(docker_image_tag,
filename=docker_file_name,

Просмотреть файл

@ -24,6 +24,7 @@ speckle_image('frontend-2')
speckle_image('monitor-deployment')
speckle_image('preview-service')
speckle_image('server')
speckle_image('objects','server')
speckle_image('test-deployment')
speckle_image('webhook-service')
@ -51,6 +52,7 @@ helm_resource('postgresql',
flags=['--version=^12.0.0',
'--values=./values/postgres.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/postgres.values.yaml'],
labels=['speckle-dependencies'])
helm_resource('minio',
@ -60,6 +62,7 @@ helm_resource('minio',
flags=['--version=^12.0.0',
'--values=./values/minio.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/minio.values.yaml'],
labels=['speckle-dependencies'])
helm_resource('redis',
@ -69,6 +72,7 @@ helm_resource('redis',
flags=['--version=18.7.1',
'--values=./values/redis.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/redis.values.yaml'],
labels=['speckle-dependencies'])
#FIXME this helm chart does not deploy any containers, so tilt incorrectly believes it never gets to a final state
@ -79,6 +83,7 @@ helm_resource('redis',
# namespace='prometheus',
# resource_deps=['prometheus-repo'],
# chart='prometheus-repo/prometheus-operator-crds',
# deps=['./values/prometheus-operator-crds.values.yaml'],
# flags=['--version=^7.0.0',
# '--values=./values/prometheus-operator-crds.values.yaml',
# '--kube-context=kind-speckle-server'])
@ -95,6 +100,7 @@ helm_resource('ingress-nginx',
flags=['--version=^4.8.0',
'--values=./values/nginx.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/nginx.values.yaml'],
resource_deps=['postgresql', 'minio', 'redis', 'ingress-nginx-repo'],
labels=['speckle-dependencies'])
@ -122,6 +128,7 @@ helm_resource('speckle-server',
'speckle/speckle-monitor-deployment',
'speckle/speckle-preview-service',
'speckle/speckle-server',
'speckle/speckle-objects',
'speckle/speckle-test-deployment',
'speckle/speckle-webhook-service',
],
@ -131,6 +138,7 @@ helm_resource('speckle-server',
'monitor.image',
'preview_service.image',
'server.image',
'objects.image',
'test.image',
'webhook_service.image',
],

Просмотреть файл

@ -1,11 +1,16 @@
# Build each Dockerfile and reference resource for use in the primary Tiltfile
# The referenced resources can then be deployed by the Helm Chart
def speckle_image(package):
package_dir = 'packages/{}'.format(package)
def speckle_image(package,original_package_name=None):
if not original_package_name:
original_package_name = package
package_dir = 'packages/{}'.format(original_package_name)
if package == 'test-deployment' or package == 'monitor-deployment' or package == 'docker-compose-ingress':
package_dir = 'utils/{}'.format(package)
docker_build('speckle/speckle-{}'.format(package),
context='../..',
dockerfile='../../{}/Dockerfile'.format(package_dir),
platform='linux/amd64')
platform='linux/amd64',
ignore = ['**/node_modules', '**/dist', '**/build', '**/coverage', 'minio-data', 'postgres-data']
)

Просмотреть файл

@ -25,6 +25,23 @@ server:
disable_tracking: true
disable_tracing: true
objects:
replicas: 1
# session_secret: secret -> `session_secret`
auth:
local:
enabled: true
logLevel: debug
email:
enabled: false
monitoring:
mp:
enabled: false
disable_tracking: true
disable_tracing: true
frontend_2:
enabled: true

Просмотреть файл

@ -11,9 +11,9 @@ if [[ "${CIRCLE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
fi
if [[ "${CIRCLE_BRANCH}" == "main" ]]; then
echo "$NEXT_RELEASE-alpha.${CIRCLE_BUILD_NUM}"
echo "${NEXT_RELEASE}-alpha.${CIRCLE_BUILD_NUM}"
exit 0
fi
echo "$NEXT_RELEASE-branch.${BRANCH_NAME_TRUNCATED}.${CIRCLE_BUILD_NUM}-${COMMIT_SHA1_TRUNCATED}"
echo "${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${CIRCLE_BUILD_NUM}-${COMMIT_SHA1_TRUNCATED}"
exit 0

Просмотреть файл

@ -35,8 +35,9 @@ import {
ExclamationTriangleIcon
} from '@heroicons/vue/24/outline'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
import { useFilterUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
import type { NumericPropertyInfo } from '@speckle/viewer'
import { containsAll } from '~~/lib/common/helpers/utils'
type ObjectResultWithOptionalMetadata = {
category: string
@ -62,8 +63,7 @@ const {
const { isolateObjects, resetFilters, setPropertyFilter, applyPropertyFilter } =
useFilterUtilities()
import { containsAll } from '~~/lib/common/helpers/utils'
const { setSelectionFromObjectIds, clearSelection } = useSelectionUtilities()
const hasMetadataGradient = computed(() => {
if (props.result.metadata?.gradient) return true
@ -72,7 +72,7 @@ const hasMetadataGradient = computed(() => {
const isolatedObjects = computed(() => filteringState.value?.isolatedObjects)
const isIsolated = computed(() => {
if (!isolatedObjects.value) return false
if (!isolatedObjects.value?.length) return false
if (filteringState.value?.activePropFilterKey === props.functionId) return false
const ids = props.result.objectIds
return containsAll(ids, isolatedObjects.value)
@ -83,17 +83,21 @@ const handleClick = () => {
setOrUnsetGradient()
return
}
isolateOrUnisolateObjects()
}
const isolateOrUnisolateObjects = () => {
const ids = props.result.objectIds
if (!isIsolated.value) {
resetFilters()
isolateObjects(ids)
return
}
const isCurrentlyIsolated = isIsolated.value
resetFilters()
if (isCurrentlyIsolated) {
clearSelection()
} else {
isolateObjects(ids)
setSelectionFromObjectIds(ids)
}
}
const metadataGradientIsSet = ref(false)

Просмотреть файл

@ -5,7 +5,7 @@
>
<!-- Add new thread bubble -->
<ViewerAnchoredPointNewThread
v-if="!isEmbedEnabled"
v-if="shouldShowNewThread"
v-model="buttonState"
:can-post-comment="canPostComment"
class="z-[13]"
@ -89,25 +89,27 @@
class="w-full h-full outline -outline-offset-0 outline-8 rounded-md outline-blue-500/40"
>
<div class="absolute bottom-4 right-4 p-2 pointer-events-auto">
<FormButton
v-if="spotlightUserSessionId && spotlightUser"
size="xs"
class="truncate"
@click="() => (spotlightUserSessionId = null)"
>
<span>Stop Following {{ spotlightUser?.userName.split(' ')[0] }}</span>
</FormButton>
<div
v-else
v-tippy="followers.map((u) => u.name).join(', ')"
class="text-xs p-2 font-bold text-primary"
>
Followed by {{ followers[0].name.split(' ')[0] }}
<span v-if="followers.length > 1">
& {{ followers.length - 1 }}
{{ followers.length - 1 === 1 ? 'other' : 'others' }}
</span>
</div>
<Portal to="pocket-right">
<FormButton
v-if="spotlightUserSessionId && spotlightUser"
size="xs"
class="truncate"
@click="() => (spotlightUserSessionId = null)"
>
<span>Stop Following {{ spotlightUser?.userName.split(' ')[0] }}</span>
</FormButton>
<div
v-else
v-tippy="followers.map((u) => u.name).join(', ')"
class="text-xs p-2 font-bold text-primary"
>
Followed by {{ followers[0].name.split(' ')[0] }}
<span v-if="followers.length > 1">
& {{ followers.length - 1 }}
{{ followers.length - 1 === 1 ? 'other' : 'others' }}
</span>
</div>
</Portal>
</div>
</div>
</div>
@ -186,6 +188,10 @@ const onThreadExpandedChange = (isExpanded: boolean) => {
}
}
const shouldShowNewThread = computed(
() => !isEmbedEnabled.value && !state.ui.measurement.enabled.value
)
const allThreadsChronologicalOrder = computed(() => {
const vals = Object.values(commentThreads.value)
return vals.sort(

Просмотреть файл

@ -11,11 +11,7 @@
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<FormButton
:size="isEmbedEnabled ? 'xs' : 'sm'"
class="pointer-events-auto"
@click="trackAndResetFilters"
>
<FormButton size="xs" class="pointer-events-auto" @click="trackAndResetFilters">
Reset Filters
</FormButton>
</Transition>
@ -24,14 +20,12 @@
<script setup lang="ts">
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
const {
resetFilters,
filters: { hasAnyFiltersApplied }
} = useFilterUtilities()
const { isEnabled: isEmbedEnabled } = useEmbed()
const mp = useMixpanel()
const trackAndResetFilters = () => {
resetFilters()

Просмотреть файл

@ -63,14 +63,22 @@
</div>
</Transition>
<div
class="absolute z-10 w-screen flex flex-col items-center justify-center gap-2"
class="absolute z-10 w-screen px-8 grid grid-cols-1 sm:grid-cols-3 gap-2"
:class="isEmbedEnabled ? 'bottom-16 mb-1' : 'bottom-6'"
>
<PortalTarget name="pocket-tip"></PortalTarget>
<div class="flex gap-3">
<PortalTarget name="pocket-actions"></PortalTarget>
<!-- Shows up when filters are applied for an easy return to normality -->
<ViewerGlobalFilterReset class="z-20" :embed="!!isEmbedEnabled" />
<div class="flex items-end justify-center sm:justify-start">
<PortalTarget name="pocket-left"></PortalTarget>
</div>
<div class="flex flex-col gap-2 items-center justify-end">
<PortalTarget name="pocket-tip"></PortalTarget>
<div class="flex gap-3">
<PortalTarget name="pocket-actions"></PortalTarget>
<!-- Shows up when filters are applied for an easy return to normality -->
<ViewerGlobalFilterReset class="z-20" :embed="!!isEmbedEnabled" />
</div>
</div>
<div class="flex items-end justify-center sm:justify-end">
<PortalTarget name="pocket-right"></PortalTarget>
</div>
</div>
</ClientOnly>

Просмотреть файл

@ -0,0 +1,135 @@
<template>
<div
ref="resizableElement"
class="relative sm:absolute z-10 right-0 h-[50dvh] overflow-hidden w-screen sm:pr-3 sm:pb-3 sm:pt-0"
:style="!isSmallerOrEqualSm ? { maxWidth: width + 'px' } : {}"
:class="[
open ? '' : 'pointer-events-none',
isEmbedEnabled === true
? 'sm:top-2 sm:h-[calc(100dvh-3.8rem)]'
: 'sm:top-[4.2rem] sm:h-[calc(100dvh-4.2rem)]'
]"
>
<div
class="flex transition-all h-full"
:class="open ? '' : 'sm:translate-x-[100%]'"
>
<!-- Resize Handle -->
<div
ref="resizeHandle"
class="hidden sm:flex group relative z-30 hover:z-50 w-6 h-full items-center overflow-hidden -mr-1"
>
<div
class="w-5 h-8 mr-1 bg-foundation group-hover:bg-outline-2 rounded-l translate-x-3 group-hover:translate-x-0.5 transition cursor-ew-resize flex items-center justify-center group-hover:shadow-xl"
@mousedown="startResizing"
>
<ArrowsRightLeftIcon
class="h-3 w-3 transition opacity-0 group-hover:opacity-80 text-outline-1 -ml-px"
/>
</div>
<div
class="relative z-30 w-1 h-full pt-[4.2rem] -ml-1 bg-transparent group-hover:bg-outline-2 cursor-ew-resize transition rounded-l"
@mousedown="startResizing"
></div>
</div>
<div
class="flex flex-col w-full h-full relative z-20 overflow-hidden shadow-lg rounded-b-md"
>
<!-- Header -->
<div
class="h-18 absolute z-10 top-0 w-full left-0 bg-foundation shadow-md sm:rounded-t-md"
>
<div
class="flex items-center justify-between pl-3 pr-2.5 h-10 border-b border-outline-3"
>
<div v-if="$slots.title" class="font-bold text-sm text-primary">
<slot name="title"></slot>
</div>
<div class="flex items-center gap-0.5">
<button class="p-0.5 text-foreground hover:text-primary" @click="onClose">
<XMarkIcon class="h-4 w-4" />
</button>
</div>
</div>
<div v-if="$slots.actions" class="w-full px-3 h-8">
<div class="flex items-center gap-1 h-full">
<slot name="actions"></slot>
</div>
</div>
</div>
<div class="w-full" :class="$slots.actions ? 'h-24 sm:h-20' : 'h-10'"></div>
<div
class="overflow-y-auto simple-scrollbar h-[calc(50dvh)] sm:h-full bg-foundation w-full pt-2 sm:rounded-b-md"
>
<slot></slot>
</div>
<div
v-if="$slots.footer"
class="absolute z-20 bottom-0 h-8 bg-foundation shadow-t w-full flex items-center px-3 empty:translate-y-10 transition sm:rounded-b-md"
>
<slot name="footer"></slot>
</div>
<div v-if="$slots.footer" class="h-8"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useEventListener } from '@vueuse/core'
import { XMarkIcon, ArrowsRightLeftIcon } from '@heroicons/vue/24/outline'
import { useIsSmallerOrEqualThanBreakpoint } from '~~/composables/browser'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
defineProps<{
open: boolean
}>()
const emit = defineEmits<{
(event: 'close'): void
}>()
const resizableElement = ref(null)
const resizeHandle = ref(null)
const isResizing = ref(false)
const width = ref(300)
let startWidth = 0
let startX = 0
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
const { isEnabled: isEmbedEnabled } = useEmbed()
const startResizing = (event: MouseEvent) => {
event.preventDefault()
isResizing.value = true
startX = event.clientX
startWidth = width.value
}
useEventListener(resizeHandle, 'mousedown', startResizing)
useEventListener(document, 'mousemove', (event) => {
if (isResizing.value) {
const diffX = startX - event.clientX
width.value = Math.max(
300,
Math.min(startWidth + diffX, (parseInt('75vw') * window.innerWidth) / 100)
)
}
})
useEventListener(document, 'mouseup', () => {
if (isResizing.value) {
isResizing.value = false
}
})
const minimize = () => {
width.value = 300
}
const onClose = () => {
minimize()
emit('close')
}
</script>

Просмотреть файл

@ -16,9 +16,9 @@
color="danger"
:icon-left="TrashIcon"
class="font-normal py-1"
@click="() => removeMeasurement()"
@click="() => clearMeasurements()"
>
Delete Selected
Delete All Measurements
</FormButton>
</template>
<div class="px-3 py-2 sm:p-3 flex flex-col gap-3 border-b border-outline-3">
@ -75,13 +75,13 @@
</div>
</div>
<Portal to="pocket-tip">
<ViewerTip>
<ViewerTip class="hidden sm:flex">
<strong>Tip:</strong>
Right click to cancel measurement
</ViewerTip>
</Portal>
<Portal to="pocket-actions">
<FormButton size="sm" @click="() => clearMeasurements()">
<FormButton size="xs" @click="() => clearMeasurements()">
Reset Measurements
</FormButton>
</Portal>
@ -113,8 +113,7 @@ const measurementParams = ref({
precision: measurementPrecision.value
})
const { setMeasurementOptions, removeMeasurement, clearMeasurements } =
useMeasurementUtilities()
const { setMeasurementOptions, clearMeasurements } = useMeasurementUtilities()
const updateMeasurementsType = (selectedOption: MeasurementTypeOption) => {
measurementParams.value.type = selectedOption.value

Просмотреть файл

@ -8,10 +8,15 @@
>
<div class="mb-1 flex items-center">
<button
class="flex h-full w-full p-2 items-center justify-between gap-4 rounded bg-foundation-2 hover:sm:bg-primary-muted hover:text-primary"
class="flex h-full w-full px-2 py-0.5 items-center gap-1 rounded bg-foundation-2 hover:sm:bg-primary-muted hover:text-primary"
:class="unfold && 'text-primary'"
@click="unfold = !unfold"
>
<ChevronDownIcon
:class="`h-3 w-3 transition ${headerClasses} ${
!unfold ? '-rotate-90' : 'rotate-0'
}`"
/>
<div :class="`truncate text-xs font-bold ${headerClasses}`">
{{ title || headerAndSubheader.header }}
<span
@ -20,39 +25,57 @@
{{ isModifiedQuery.isNew ? '(New)' : '(Old)' }}
</span>
</div>
<ChevronDownIcon
:class="`h-3 w-3 transition ${headerClasses} ${
!unfold ? '-rotate-90' : 'rotate-0'
}`"
/>
</button>
</div>
<div v-if="unfold" class="ml-1 space-y-1 p-2">
<div v-if="unfold" class="ml-1 space-y-1 px-2 py-1">
<div
v-for="(kvp, index) in [
...categorisedValuePairs.primitives,
...categorisedValuePairs.nulls
]"
:key="index"
class="flex w-full"
>
<div
:class="`grid grid-cols-3 ${
:class="`grid grid-cols-3 w-full pl-2 ${
kvp.value === null || kvp.value === undefined ? 'text-foreground-2' : ''
}`"
>
<div
class="col-span-1 truncate text-xs font-bold"
class="col-span-1 truncate text-xs font-bold mr-2"
:title="(kvp.key as string)"
>
{{ kvp.key }}
</div>
<div class="col-span-2 pl-1 truncate text-xs" :title="(kvp.value as string)">
<!-- NOTE: can't do kvp.value || 'null' because 0 || 'null' = 'null' -->
{{ kvp.value === null ? 'null' : kvp.value }}
<div
class="group col-span-2 pl-1 truncate text-xs flex gap-1 items-center"
:title="(kvp.value as string)"
>
<div class="flex gap-1 items-center w-full">
<!-- NOTE: can't do kvp.value || 'null' because 0 || 'null' = 'null' -->
<span
class="truncate"
:class="kvp.value === null ? '' : 'group-hover:max-w-[calc(100%-1rem)]'"
>
{{ kvp.value === null ? 'null' : kvp.value }}
</span>
<button
v-if="isCopyable(kvp)"
:class="isCopyable(kvp) ? 'cursor-pointer' : 'cursor-default'"
class="opacity-0 group-hover:opacity-100 w-4"
@click="handleCopy(kvp)"
>
<ClipboardDocumentIcon class="h-3 w-3" />
</button>
</div>
</div>
</div>
</div>
<div v-for="(kvp, index) in categorisedValuePairs.objects" :key="index">
<div
v-for="(kvp, index) in categorisedValuePairs.objects"
:key="index"
class="pl-2"
>
<ViewerSelectionObject
:object="(kvp.value as Record<string,unknown>) || {}"
:title="(kvp.key as string)"
@ -96,6 +119,7 @@
<script setup lang="ts">
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { ChevronDownIcon } from '@heroicons/vue/24/solid'
import { ClipboardDocumentIcon } from '@heroicons/vue/24/outline'
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
import { getHeaderAndSubheaderForSpeckleObject } from '~~/lib/object-sidebar/helpers'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
@ -186,6 +210,21 @@ const headerAndSubheader = computed(() => {
return getHeaderAndSubheaderForSpeckleObject(props.object)
})
const isCopyable = (kvp: Record<string, unknown>) => {
return kvp.value !== null && kvp.value !== undefined && typeof kvp.value !== 'object'
}
const handleCopy = async (kvp: Record<string, unknown>) => {
const { copy } = useClipboard()
if (isCopyable(kvp)) {
const keyName = kvp.key as string
await copy(kvp.value as string, {
successMessage: `${keyName} copied to clipboard`,
failureMessage: `Failed to copy ${keyName} to clipboard`
})
}
}
const ignoredProps = [
'__closure',
'displayMesh',

Просмотреть файл

@ -1,70 +1,56 @@
<template>
<ViewerCommentsPortalOrDiv v-if="objects.length !== 0" to="bottomPanel">
<div
:class="`sm:bg-foundation simple-scrollbar z-10 relative sm:fixed sm:right-4 sm:right-4 sm:mb-4 sm:max-w-64 min-h-[3rem] sm:min-h-[4.75rem] max-h-[50vh] sm:max-h-[calc(100vh-5.5rem)] w-full sm:w-64 overflow-y-auto sm:rounded-md sm:shadow transition ${
objects.length !== 0
? 'translate-x-0 opacity-100'
: 'translate-x-[120%] opacity-0'
} ${isEmbedEnabled ? 'sm:top-2' : 'sm:top-[4rem]'} ${
focusedThreadId && isSmallerOrEqualSm ? 'hidden' : ''
}`"
>
<ViewerLayoutPanel @close="trackAndClearSelection()">
<template #title>Selection Info</template>
<template #actions>
<FormButton
size="xs"
color="secondary"
class="opacity-80 hover:opacity-100"
@click.stop="hideOrShowSelection"
>
<div v-if="isHidden" class="flex items-center gap-1">
<EyeIcon class="h-4 w-4" />
Show
</div>
<div v-else class="flex items-center gap-1">
<EyeSlashIcon class="h-4 w-4" />
Hide
</div>
</FormButton>
<FormButton
size="xs"
color="secondary"
class="hover:opacity-100"
:class="isIsolated ? 'text-primary opacity-100' : 'opacity-80'"
@click.stop="isolateOrUnisolateSelection"
>
<div class="flex items-center gap-1">
<FunnelIconOutline v-if="!isIsolated" class="h-4 w-4" />
<FunnelIcon v-else class="h-4 w-4" />
Isolate
</div>
</FormButton>
</template>
<div class="p-1 mb-2 sm:mb-0 sm:py-2 sm:bg-white/90 dark:sm:bg-neutral-700/90">
<div class="space-y-2">
<ViewerSelectionObject
v-for="object in objectsLimited"
:key="(object.id as string)"
:object="object"
:unfold="false"
:root="true"
/>
<ViewerCommentsPortalOrDiv v-if="shouldRenderSidebar" to="bottomPanel">
<ViewerSidebar :open="sidebarOpen" @close="onClose">
<template #title><div class="select-none">Selection Info</div></template>
<template #actions>
<FormButton
size="xs"
color="secondary"
class="opacity-80 hover:opacity-100"
@click.stop="hideOrShowSelection"
>
<div class="flex items-center gap-1">
<EyeIcon v-if="!isHidden" class="h-4 w-4" />
<EyeSlashIcon v-else class="h-4 w-4" />
Hide
</div>
<div v-if="itemCount <= objects.length" class="mb-2">
<FormButton size="xs" text full-width @click="itemCount += 10">
View More ({{ objects.length - itemCount }})
</FormButton>
</div>
<div
v-if="objects.length === 1"
class="hidden sm:block text-foreground-2 mt-2 px-2 text-xs"
>
Hold "shift" to select multiple objects
</FormButton>
<FormButton
size="xs"
color="secondary"
class="hover:opacity-100"
:class="isIsolated ? 'text-primary opacity-100' : 'opacity-80'"
@click.stop="isolateOrUnisolateSelection"
>
<div class="flex items-center gap-1">
<FunnelIconOutline v-if="!isIsolated" class="h-4 w-4" />
<FunnelIcon v-else class="h-4 w-4" />
Isolate
</div>
</FormButton>
</template>
<div class="p-1 mb-2 sm:mb-0 sm:py-2">
<div class="space-y-2">
<ViewerSelectionObject
v-for="object in objectsLimited"
:key="(object.id as string)"
:object="object"
:root="true"
:unfold="objectsLimited.length === 1"
/>
</div>
</ViewerLayoutPanel>
</div>
<div v-if="itemCount <= objects.length" class="mb-2">
<FormButton size="xs" text full-width @click="itemCount += 10">
View More ({{ objects.length - itemCount }})
</FormButton>
</div>
</div>
<template v-if="!isSmallerOrEqualSm" #footer>
<div class="text-foreground-2 text-xs select-none">
Hold "shift" to select multiple objects
</div>
</template>
</ViewerSidebar>
</ViewerCommentsPortalOrDiv>
</template>
<script setup lang="ts">
@ -79,23 +65,21 @@ import { useFilterUtilities, useSelectionUtilities } from '~~/lib/viewer/composa
import { uniqWith } from 'lodash-es'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useIsSmallerOrEqualThanBreakpoint } from '~~/composables/browser'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
const {
viewer: {
metadata: { filteringState }
},
urlHashState: { focusedThreadId },
ui: { diff }
ui: { diff, measurement }
} = useInjectedViewerState()
const { objects, clearSelection } = useSelectionUtilities()
const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
useFilterUtilities()
const { isEmbedEnabled } = useEmbed()
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
const itemCount = ref(42)
const itemCount = ref(20)
const sidebarOpen = ref(false)
const objectsUniqueByAppId = computed(() => {
if (!diff.enabled.value) return objects.value
@ -104,6 +88,10 @@ const objectsUniqueByAppId = computed(() => {
})
})
const shouldRenderSidebar = computed(() => {
return (!isSmallerOrEqualSm.value || sidebarOpen.value) && !measurement.enabled.value
})
const objectsLimited = computed(() => {
return objectsUniqueByAppId.value.slice(0, itemCount.value)
})
@ -179,6 +167,11 @@ const trackAndClearSelection = () => {
})
}
const onClose = () => {
sidebarOpen.value = false
trackAndClearSelection()
}
onKeyStroke('Escape', () => {
// Cleareance of any vis/iso state coming from here should happen in clearSelection()
// Note: we're not using the trackAndClearSelection method beacuse
@ -191,4 +184,15 @@ onKeyStroke('Escape', () => {
source: 'keypress-escape'
})
})
watch(
() => objects.value.length,
(newLength) => {
if (newLength !== 0) {
sidebarOpen.value = true
} else {
sidebarOpen.value = false
}
}
)
</script>

Просмотреть файл

@ -6,7 +6,7 @@ export { isNonNullable } from '@speckle/shared'
* @param target the array you want to check that is included in the other one
* @param source the array you want to check INTO for inclusion of the previous one
*/
export const containsAll = (target: unknown[], source: unknown[]) =>
export const containsAll = <T>(target: T[], source: T[]) =>
target.every((v) => source.includes(v))
/**
@ -24,18 +24,34 @@ const length = (vals: unknown[] | Set<unknown>) =>
isSet(vals) ? vals.size : vals.length
/**
* A performant way to check if two arrays/sets have at least one element in common
* Various performance-improved array utilities:
*/
/**
* Whenever you have to compare two arrays/sets, this function will make sure the biggest one is a Set for quick look ups
* and so you can iterate over the smallest one
* @param vals1
* @param vals2
*/
const toOptimizedComparisonArrays = <V = unknown>(
vals1: V[] | Set<V>,
vals2: V[] | Set<V>
) => {
const biggest: Set<V> = length(vals1) > length(vals2) ? toSet(vals1) : toSet(vals2)
const smallest: V[] | Set<V> = length(vals1) > length(vals2) ? vals2 : vals1
return { biggest, smallest: isSet(smallest) ? [...smallest] : smallest }
}
/**
* A fast way to check if two arrays/sets have at least one element in common
*/
export const hasIntersection = <V = unknown>(
vals1: V[] | Set<V>,
vals2: V[] | Set<V>
) => {
if (!length(vals1) || !length(vals2)) return false
const { biggest, smallest } = toOptimizedComparisonArrays(vals1, vals2)
if (!length(biggest) || !length(smallest)) return false
// Always iterating over the smallest collection to speed things up, and making
// sure the biggest one is a Set for quick look ups
const biggest: Set<V> = length(vals1) > length(vals2) ? toSet(vals1) : toSet(vals2)
const smallest: V[] | Set<V> = length(vals1) > length(vals2) ? vals2 : vals1
return (isSet(smallest) ? [...smallest] : smallest).some((v) => biggest.has(v))
return smallest.some((v) => biggest.has(v))
}

Просмотреть файл

@ -0,0 +1,28 @@
import { Redis } from 'ioredis'
import type pino from 'pino'
export const createRedis = async (params: { logger: pino.Logger }) => {
const { logger } = params
const { redisUrl } = useRuntimeConfig()
if (!redisUrl?.length) {
return undefined
}
const redis = new Redis(redisUrl)
redis.on('error', (err) => {
logger.error(err, 'Redis error')
})
redis.on('end', () => {
logger.info('Redis disconnected from server')
})
// Try to ping the server
const res = await redis.ping()
if (res !== 'PONG') {
throw new Error('Redis server did not respond to ping')
}
return redis
}

Просмотреть файл

@ -242,6 +242,19 @@ export function useSelectionUtilities() {
selectedObjects.value = objs
}
const addToSelectionFromObjectIds = (objectIds: string[]) => {
const originalObjects = selectedObjects.value.slice()
setSelectionFromObjectIds(objectIds)
selectedObjects.value = [...originalObjects, ...selectedObjects.value]
}
const removeFromSelectionObjectIds = (objectIds: string[]) => {
const finalObjects = selectedObjects.value.filter(
(o) => !objectIds.includes(o.id || '')
)
selectedObjects.value = finalObjects
}
const addToSelection = (object: SpeckleObject) => {
const idx = selectedObjects.value.findIndex((o) => o.id === object.id)
if (idx !== -1) return
@ -268,6 +281,8 @@ export function useSelectionUtilities() {
removeFromSelection,
clearSelection,
setSelectionFromObjectIds,
addToSelectionFromObjectIds,
removeFromSelectionObjectIds,
objects: selectedObjects,
objectIds: selectedObjectIds
}

Просмотреть файл

@ -120,7 +120,7 @@
"tailwindcss": "^3.4.1",
"type-fest": "^3.5.1",
"typescript": "^4.8.3",
"vue-tsc": "1.8.22",
"vue-tsc": "1.8.27",
"wait-on": "^6.0.1"
},
"engines": {

Просмотреть файл

@ -5,7 +5,7 @@ import { useCreateErrorLoggingTransport } from '~/lib/core/composables/error'
type PluginNuxtApp = Parameters<Plugin>[0]
async function initRumClient(app: PluginNuxtApp) {
const { enabled, keys, speckleServerVersion } = resolveInitParams()
const { enabled, keys, speckleServerVersion, baseUrl } = resolveInitParams()
const logger = useLogger()
const onAuthStateChange = useOnAuthStateChange()
const router = useRouter()
@ -18,8 +18,9 @@ async function initRumClient(app: PluginNuxtApp) {
rg4js('apiKey', keys.raygun)
rg4js('enableCrashReporting', true)
rg4js('enablePulse', true)
rg4js('boot')
rg4js('enableRum', true)
// rg4js('boot')
// rg4js('enableRum', true)
rg4js('withTags', [`baseUrl:${baseUrl}`, `version:${speckleServerVersion}`])
await onAuthStateChange(
(user, { resolveDistinctId }) => {
@ -184,7 +185,8 @@ function resolveInitParams() {
logrocketAppId,
speckleServerVersion,
speedcurveId,
debugbearId
debugbearId,
baseUrl
}
} = useRuntimeConfig()
const raygun = raygunKey?.length ? raygunKey : null
@ -201,7 +203,8 @@ function resolveInitParams() {
speedcurve,
debugbear
},
speckleServerVersion
speckleServerVersion,
baseUrl
}
}

Просмотреть файл

@ -1,4 +1,5 @@
import { Redis } from 'ioredis'
import { createRedis } from '~/lib/core/helpers/redis'
/**
* Re-using the same client for all SSR reqs (shouldn't be a problem)
@ -9,31 +10,20 @@ let redis: InstanceType<typeof Redis> | undefined = undefined
* Provide redis (only in SSR)
*/
export default defineNuxtPlugin(async () => {
const { redisUrl } = useRuntimeConfig()
const logger = useLogger()
if (redisUrl?.length) {
try {
const hasValidStatus =
redis && ['ready', 'connecting', 'reconnecting'].includes(redis.status)
if (!redis || !hasValidStatus) {
if (redis) {
await redis.quit()
}
redis = new Redis(redisUrl)
redis.on('error', (err) => {
logger.error(err, 'Redis error')
})
redis.on('end', () => {
logger.info('Redis disconnected from server')
})
try {
const hasValidStatus =
redis && ['ready', 'connecting', 'reconnecting'].includes(redis.status)
if (!redis || !hasValidStatus) {
if (redis) {
await redis.quit()
}
} catch (e) {
logger.error(e, 'Redis setup failure')
redis = await createRedis({ logger })
}
} catch (e) {
logger.error(e, 'Redis setup failure')
}
const isValid = redis && redis.status === 'ready'

Просмотреть файл

@ -1,6 +1,28 @@
import { useRequestId } from '~/lib/core/composables/server'
import { ensureError } from '@speckle/shared'
import { createRedis } from '~/lib/core/helpers/redis'
export default defineEventHandler((event) => {
const reqId = useRequestId({ event })
return { status: 'ok', reqId }
/**
* Check that the deployment is fine
*/
export default defineEventHandler(async () => {
let redisConnected = false
// Check that redis works
try {
const redis = await createRedis({ logger: useLogger() })
redisConnected = !!redis
if (redis) {
await redis.quit()
}
} catch (e) {
const errMsg = ensureError(e).message
throw createError({
statusCode: 500,
fatal: true,
message: `Redis connection failed: ${errMsg}`
})
}
return { status: 'ok', redisConnected }
})

Просмотреть файл

@ -3,6 +3,7 @@ import { Observability } from '@speckle/shared'
import type { IncomingMessage } from 'node:http'
import { get } from 'lodash-es'
import type { Logger } from 'pino'
import type express from 'express'
const redactedReqHeaders = ['authorization', 'cookie']
@ -44,7 +45,7 @@ export function serializeRequest(req: IncomingMessage) {
return {
id: req.id,
method: req.method,
path: req.url?.split('?')[0], // Remove query params which might be sensitive
path: getRequestPath(req),
// Allowlist useful headers
headers: Object.keys(req.headers).reduce((obj, key) => {
let valueToPrint = req.headers[key]
@ -58,3 +59,10 @@ export function serializeRequest(req: IncomingMessage) {
}, {})
}
}
export const getRequestPath = (req: IncomingMessage | express.Request) => {
const path = ((get(req, 'originalUrl') || get(req, 'url') || '') as string).split(
'?'
)[0] as string
return path?.length ? path : null
}

Просмотреть файл

@ -1,4 +1,3 @@
import { Observability } from '@speckle/shared'
import { defineEventHandler, fromNodeMiddleware } from 'h3'
import { IncomingMessage, ServerResponse } from 'http'
import pino from 'pino'
@ -9,7 +8,10 @@ import { randomUUID } from 'crypto'
import type { IncomingHttpHeaders } from 'http'
import { REQUEST_ID_HEADER } from '~~/server/lib/core/helpers/constants'
import { get } from 'lodash'
import { serializeRequest } from '~/server/lib/core/helpers/observability'
import {
serializeRequest,
getRequestPath
} from '~/server/lib/core/helpers/observability'
/**
* Server request logger
@ -28,10 +30,7 @@ function determineRequestId(
const generateReqId: GenReqId = (req: IncomingMessage) =>
determineRequestId(req.headers)
const logger = Observability.getLogger(
useRuntimeConfig().public.logLevel,
useRuntimeConfig().public.logPretty
)
const logger = useLogger()
export const LoggingMiddleware = pinoHttp({
logger,
@ -46,8 +45,9 @@ export const LoggingMiddleware = pinoHttp({
error: Error | undefined
) => {
// Mark some lower importance/spammy endpoints w/ 'debug' to reduce noise
const path = req.url?.split('?')[0]
const shouldBeDebug = ['/metrics', '/health'].includes(path || '') ?? false
const path = getRequestPath(req)
const shouldBeDebug =
['/metrics', '/health', '/api/status'].includes(path || '') ?? false
if (res.statusCode >= 400 && res.statusCode < 500) {
return 'info'
@ -66,7 +66,7 @@ export const LoggingMiddleware = pinoHttp({
customSuccessObject(req, res, val: Record<string, unknown>) {
const isCompleted = !req.readableAborted && res.writableEnded
const requestStatus = isCompleted ? 'completed' : 'aborted'
const requestPath = req.url?.split('?')[0] || 'unknown'
const requestPath = getRequestPath(req) || 'unknown'
const appBindings = res.vueLoggerBindings || {}
return {
@ -82,7 +82,7 @@ export const LoggingMiddleware = pinoHttp({
},
customErrorObject(req, res, err, val: Record<string, unknown>) {
const requestStatus = 'failed'
const requestPath = req.url?.split('?')[0] || 'unknown'
const requestPath = getRequestPath(req) || 'unknown'
const appBindings = res.vueLoggerBindings || {}
return {
@ -107,9 +107,10 @@ export const LoggingMiddleware = pinoHttp({
const realRaw = get(res, 'raw.raw') as typeof res.raw
const isRequestCompleted = !!realRaw.writableEnded
const isRequestAborted = !isRequestCompleted
const statusCode = res.statusCode || res.raw.statusCode || realRaw.statusCode
return {
statusCode: res.raw.statusCode,
statusCode,
// Allowlist useful headers
headers: resRaw.headers,
isRequestAborted

Просмотреть файл

@ -0,0 +1,6 @@
{
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"verbatimModuleSyntax": true
}
}

Просмотреть файл

@ -0,0 +1,29 @@
import type { Optional } from '@speckle/shared'
import type pino from 'pino'
import { buildLogger } from '~/server/lib/core/helpers/observability'
let logger: Optional<pino.Logger> = undefined
const createLogger = () => {
const {
public: { logLevel, logPretty, speckleServerVersion, serverName }
} = useRuntimeConfig()
const logger = buildLogger(logLevel, logPretty).child({
browser: false,
speckleServerVersion,
serverName,
frontendType: 'frontend-2',
serverLogger: true
})
return logger
}
export const useLogger = () => {
if (!logger) {
logger = createLogger()
}
return logger
}

Просмотреть файл

@ -126,4 +126,9 @@ STRATEGY_LOCAL=true
# FRONTEND_HOST=127.0.0.1
# FRONTEND_PORT=8081
SPECKLE_AUTOMATE_URL="http://127.0.0.1:3030"
############################################################
# Speckle automate related variables
# the env var is only needed if you are running the server and
# the execution engine locally
# SPECKLE_AUTOMATE_URL="http://127.0.0.1:3030"
############################################################

Просмотреть файл

@ -12,6 +12,11 @@ const { init, startHttp } = require('../app')
init()
.then(({ app, server }) => startHttp(server, app))
.catch((err) => logger.error(err))
.catch((err) => {
logger.error(err, 'Failed to start server. Exiting with non-zero exit code...')
// kill it with fire 🔥
process.exit(1)
})
// 💥

Просмотреть файл

@ -6,6 +6,11 @@ const { init, startHttp } = require('../dist/app')
init()
.then(({ app, server }) => startHttp(server, app))
.catch((err) => logger.error(err))
.catch((err) => {
logger.error(err, 'Failed to start server. Exiting with non-zero exit code...')
// kill it with fire 🔥
process.exit(1)
})
// 💥

Просмотреть файл

@ -106,9 +106,10 @@ export const LoggingExpressMiddleware = HttpLogger({
}
const serverRes = get(res, 'raw.raw') as ServerResponse
const auth = serverRes.req.context
const statusCode = res.statusCode || res.raw.statusCode || serverRes.statusCode
return {
statusCode: res.raw.statusCode,
statusCode,
// Allowlist useful headers
headers: Object.fromEntries(
Object.entries(resRaw.raw.headers).filter(

Просмотреть файл

@ -1,220 +0,0 @@
'use strict'
const knex = require('@/db/knex')
const Scopes = () => knex('scopes')
const Apps = () => knex('server_apps')
const AppScopes = () => knex('server_apps_scopes')
const { getApp } = require('@/modules/auth/services/apps')
const { Scopes: ScopesConst } = require('@/modules/core/helpers/mainConstants')
const { difference } = require('lodash')
const { moduleLogger } = require('@/logging/logging')
const { speckleAutomateUrl } = require('@/modules/shared/helpers/envHelper')
let allScopes = []
module.exports = async () => {
allScopes = await Scopes().select('*')
// Note: shallow cloning of app objs so as to not interfere with the original objects.
await registerOrUpdateApp({ ...SpeckleWebApp })
await registerOrUpdateApp({ ...SpeckleApiExplorer })
await registerOrUpdateApp({ ...SpeckleDesktopApp })
await registerOrUpdateApp({ ...SpeckleConnectorApp })
await registerOrUpdateApp({ ...SpeckleExcel })
await registerOrUpdateApp({ ...SpecklePowerBi })
await registerOrUpdateApp({ ...SpeckleAutomate })
}
async function registerOrUpdateApp(app) {
if (app.scopes && app.scopes === 'all') {
// let scopes = await Scopes( ).select( '*' )
// logger.debug( allScopes.length )
app.scopes = allScopes.map((s) => s.name)
}
const existingApp = await getApp({ id: app.id })
if (existingApp) {
updateDefaultApp(app, existingApp)
} else {
await registerDefaultApp(app)
}
}
async function registerDefaultApp(app) {
const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s }))
delete app.scopes
await Apps().insert(app)
await AppScopes().insert(scopes)
}
async function updateDefaultApp(app, existingApp) {
const existingAppScopes = existingApp.scopes.map((s) => s.name)
const newScopes = difference(app.scopes, existingAppScopes)
const removedScopes = difference(existingAppScopes, app.scopes)
let affectedTokenIds = []
if (newScopes.length || removedScopes.length) {
moduleLogger.info(`🔑 Updating default app ${app.name}`)
affectedTokenIds = await knex('user_server_app_tokens')
.where({ appId: app.id })
.pluck('tokenId')
}
// the internal code block makes sure if an error occurred, the trx gets rolled back
await knex.transaction(async (trx) => {
// add new scopes to the app
if (newScopes.length)
await AppScopes()
.insert(newScopes.map((s) => ({ appId: app.id, scopeName: s })))
.transacting(trx)
// remove scopes from the app
if (removedScopes.length)
await AppScopes()
.where({ appId: app.id })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
//update user tokens with scope changes
if (affectedTokenIds.length)
await Promise.all(
affectedTokenIds.map(async (tokenId) => {
if (newScopes.length)
await knex('token_scopes')
.insert(newScopes.map((s) => ({ tokenId, scopeName: s })))
.transacting(trx)
if (removedScopes.length)
await knex('token_scopes')
.where({ tokenId })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
})
)
delete app.scopes
await Apps().where({ id: app.id }).update(app).transacting(trx)
})
}
// this is exported to be able to test the retention of permissions
module.exports.updateDefaultApp = updateDefaultApp
const SpeckleWebApp = {
id: 'spklwebapp',
secret: 'spklwebapp',
name: 'Speckle Web Manager',
description:
'The Speckle Web Manager is your one-stop place to manage and coordinate your data.',
trustByDefault: true,
public: true,
redirectUrl: process.env.CANONICAL_URL,
scopes: 'all'
}
const SpeckleApiExplorer = {
id: 'explorer',
secret: 'explorer',
name: 'Speckle Explorer',
description: 'GraphiQL Playground with authentication.',
trustByDefault: true,
public: true,
redirectUrl: new URL('/explorer', process.env.CANONICAL_URL).toString(),
scopes: 'all'
}
const SpeckleDesktopApp = {
id: 'sdm',
secret: 'sdm',
name: 'Speckle Desktop Manager',
description:
'Manages local installations of Speckle connectors, kits and everything else.',
trustByDefault: true,
public: true,
redirectUrl: 'speckle://account',
scopes: [
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
const SpeckleConnectorApp = {
id: 'sca',
secret: 'sca',
name: 'Speckle Connector',
description: 'A Speckle Desktop Connectors.',
trustByDefault: true,
public: true,
redirectUrl: 'http://localhost:29363',
scopes: [
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
const SpeckleExcel = {
id: 'spklexcel',
secret: 'spklexcel',
name: 'Speckle Connector For Excel',
description:
'The Speckle Connector For Excel. For more info, check the docs here: https://speckle.guide/user/excel.',
trustByDefault: true,
public: true,
redirectUrl: 'https://speckle-excel.netlify.app',
scopes: [
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
const SpecklePowerBi = {
id: 'spklpwerbi',
secret: 'spklpwerbi',
name: 'Speckle Connector For PowerBI',
description:
'The Speckle Connector For Excel. For more info check the docs here: https://speckle.guide/user/powerbi.html.',
trustByDefault: true,
public: true,
redirectUrl: 'https://oauth.powerbi.com/views/oauthredirect.html',
scopes: [
ScopesConst.Streams.Read,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
const SpeckleAutomate = {
id: 'spklautoma',
secret: 'spklautoma',
name: 'Speckle Automate',
description: 'Our automation platform',
trustByDefault: true,
public: true,
redirectUrl: `${speckleAutomateUrl()}/authn/callback`,
scopes: [
ScopesConst.Profile.Email,
ScopesConst.Profile.Read,
ScopesConst.Users.Read,
ScopesConst.Tokens.Write,
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Automate.ReportResults
]
}

Просмотреть файл

@ -0,0 +1,136 @@
import { Scopes } from '@/modules/core/helpers/mainConstants'
import { speckleAutomateUrl, getServerOrigin } from '@/modules/shared/helpers/envHelper'
const SpeckleWebApp = {
id: 'spklwebapp',
secret: 'spklwebapp',
name: 'Speckle Web Manager',
description:
'The Speckle Web Manager is your one-stop place to manage and coordinate your data.',
trustByDefault: true,
public: true,
redirectUrl: getServerOrigin(),
scopes: 'all'
}
const SpeckleApiExplorer = {
id: 'explorer',
secret: 'explorer',
name: 'Speckle Explorer',
description: 'GraphiQL Playground with authentication.',
trustByDefault: true,
public: true,
redirectUrl: new URL('/explorer', getServerOrigin()).toString(),
scopes: 'all'
}
const SpeckleDesktopApp = {
id: 'sdm',
secret: 'sdm',
name: 'Speckle Desktop Manager',
description:
'Manages local installations of Speckle connectors, kits and everything else.',
trustByDefault: true,
public: true,
redirectUrl: 'speckle://account',
scopes: [
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Profile.Read,
Scopes.Profile.Email,
Scopes.Users.Read,
Scopes.Users.Invite
]
}
const SpeckleConnectorApp = {
id: 'sca',
secret: 'sca',
name: 'Speckle Connector',
description: 'A Speckle Desktop Connectors.',
trustByDefault: true,
public: true,
redirectUrl: 'http://localhost:29363',
scopes: [
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Profile.Read,
Scopes.Profile.Email,
Scopes.Users.Read,
Scopes.Users.Invite
]
}
const SpeckleExcel = {
id: 'spklexcel',
secret: 'spklexcel',
name: 'Speckle Connector For Excel',
description:
'The Speckle Connector For Excel. For more info, check the docs here: https://speckle.guide/user/excel.',
trustByDefault: true,
public: true,
redirectUrl: 'https://speckle-excel.netlify.app',
scopes: [
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Profile.Read,
Scopes.Profile.Email,
Scopes.Users.Read,
Scopes.Users.Invite
]
}
const SpecklePowerBi = {
id: 'spklpwerbi',
secret: 'spklpwerbi',
name: 'Speckle Connector For PowerBI',
description:
'The Speckle Connector For Excel. For more info check the docs here: https://speckle.guide/user/powerbi.html.',
trustByDefault: true,
public: true,
redirectUrl: 'https://oauth.powerbi.com/views/oauthredirect.html',
scopes: [
Scopes.Streams.Read,
Scopes.Profile.Read,
Scopes.Profile.Email,
Scopes.Users.Read,
Scopes.Users.Invite
]
}
const SpeckleAutomate = {
id: 'spklautoma',
secret: 'spklautoma',
name: 'Speckle Automate',
description: 'Our automation platform',
trustByDefault: true,
public: true,
redirectUrl: `${speckleAutomateUrl()}/authn/callback`,
scopes: [
Scopes.Profile.Email,
Scopes.Profile.Read,
Scopes.Users.Read,
Scopes.Tokens.Write,
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Automate.ReportResults
]
}
const defaultApps = [
SpeckleWebApp,
SpeckleApiExplorer,
SpeckleDesktopApp,
SpeckleConnectorApp,
SpeckleExcel,
SpecklePowerBi,
SpeckleAutomate
]
export function getDefaultApps() {
return defaultApps
}
export function getDefaultApp({ id }: { id: string }) {
return defaultApps.find((app) => app.id === id) || null
}

Просмотреть файл

@ -21,5 +21,5 @@ exports.init = async (app) => {
exports.finalize = async () => {
// Note: we're registering the default apps last as we want to ensure that all
// scopes have been registered by any other modules.
await require('./defaultApps')()
await require('./manageDefaultApps')()
}

Просмотреть файл

@ -0,0 +1,100 @@
'use strict'
const knex = require('@/db/knex')
const Scopes = () => knex('scopes')
const Apps = () => knex('server_apps')
const AppScopes = () => knex('server_apps_scopes')
const { getApp } = require('@/modules/auth/services/apps')
const { difference } = require('lodash')
const { moduleLogger } = require('@/logging/logging')
const { getDefaultApps } = require('@/modules/auth/defaultApps')
let allScopes = []
module.exports = async () => {
allScopes = await Scopes().select('*')
// Note: shallow cloning of app objs so as to not interfere with the original objects.
await Promise.all(getDefaultApps().map((app) => registerOrUpdateApp({ ...app })))
}
async function registerOrUpdateApp(app) {
if (app.scopes && app.scopes === 'all') {
// let scopes = await Scopes( ).select( '*' )
// logger.debug( allScopes.length )
app.scopes = allScopes.map((s) => s.name)
}
const existingApp = await getApp({ id: app.id })
if (existingApp) {
updateDefaultApp(app, existingApp)
} else {
await registerDefaultApp(app)
}
}
async function registerDefaultApp(app) {
const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s }))
delete app.scopes
await Apps().insert(app)
await AppScopes().insert(scopes)
}
async function updateDefaultApp(app, existingApp) {
const existingAppScopes = existingApp.scopes.map((s) => s.name)
const newScopes = difference(app.scopes, existingAppScopes)
const removedScopes = difference(existingAppScopes, app.scopes)
let affectedTokenIds = []
if (newScopes.length || removedScopes.length) {
moduleLogger.info(`🔑 Updating default app ${app.name}`)
affectedTokenIds = await knex('user_server_app_tokens')
.where({ appId: app.id })
.pluck('tokenId')
}
// the internal code block makes sure if an error occurred, the trx gets rolled back
await knex.transaction(async (trx) => {
// add new scopes to the app
if (newScopes.length)
await AppScopes()
.insert(newScopes.map((s) => ({ appId: app.id, scopeName: s })))
.transacting(trx)
// remove scopes from the app
if (removedScopes.length)
await AppScopes()
.where({ appId: app.id })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
//update user tokens with scope changes
if (affectedTokenIds.length)
await Promise.all(
affectedTokenIds.map(async (tokenId) => {
if (newScopes.length)
await knex('token_scopes')
.insert(newScopes.map((s) => ({ tokenId, scopeName: s })))
.transacting(trx)
if (removedScopes.length)
await knex('token_scopes')
.where({ tokenId })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
})
)
delete app.scopes
// not writing the redirect url to the DB anymore
// it will be patched on an application level from the default app definitions
delete app.redirectUrl
await Apps().where({ id: app.id }).update(app).transacting(trx)
})
}
// this is exported to be able to test the retention of permissions
module.exports.updateDefaultApp = updateDefaultApp

Просмотреть файл

@ -5,6 +5,7 @@ const knex = require(`@/db/knex`)
const { createBareToken, createAppToken } = require(`@/modules/core/services/tokens`)
const { logger } = require('@/logging/logging')
const { getDefaultApp } = require('@/modules/auth/defaultApps')
const Users = () => knex('users')
const ApiTokens = () => knex('api_tokens')
const ServerApps = () => knex('server_apps')
@ -14,6 +15,12 @@ const Scopes = () => knex('scopes')
const AuthorizationCodes = () => knex('authorization_codes')
const RefreshTokens = () => knex('refresh_tokens')
const addDefaultAppOverrides = (app) => {
const defaultApp = getDefaultApp({ id: app.id })
if (defaultApp) app.redirectUrl = defaultApp.redirectUrl
return app
}
module.exports = {
async getApp({ id }) {
const allScopes = await Scopes().select('*')
@ -30,7 +37,8 @@ module.exports = {
.select('id', 'name', 'avatar')
.where({ id: app.authorId })
.first()
return app
return addDefaultAppOverrides(app)
},
async getAllPublicApps() {
@ -40,6 +48,7 @@ module.exports = {
'server_apps.name',
'server_apps.description',
'server_apps.trustByDefault',
'server_apps.redirectUrl',
'server_apps.logo',
'server_apps.termsAndConditionsLink',
'users.name as authorName',
@ -50,6 +59,8 @@ module.exports = {
.orderBy('server_apps.trustByDefault', 'DESC')
apps.forEach((app) => {
app = addDefaultAppOverrides(app)
if (app.authorName && app.authorId) {
app.author = { name: app.authorName, id: app.authorId }
}
@ -100,10 +111,13 @@ module.exports = {
)
const { rows } = await query
return rows.map((r) => ({
...r,
author: r.author?.id ? r.author : null
}))
return rows.map((r) => {
const app = {
...r,
author: r.author?.id ? r.author : null
}
return addDefaultAppOverrides(app)
})
},
async createApp(app) {

Просмотреть файл

@ -1,52 +1,63 @@
/* eslint-disable camelcase */
import mailchimp from '@mailchimp/mailchimp_marketing'
import { logger } from '@/logging/logging'
import { md5 } from '@/modules/shared/helpers/cryptoHelper'
import {
getMailchimpConfig,
getMailchimpStatus
} from '@/modules/shared/helpers/envHelper'
import { getUserById } from '@/modules/core/services/users'
import { getMailchimpConfig } from '@/modules/shared/helpers/envHelper'
// import { getUserById } from '@/modules/core/services/users'
import { UserRecord } from '@/modules/core/helpers/types'
async function addToMailchimpAudience(userId: string) {
// Do not do anything (inc. logging) if we do not explicitely enable it
if (!getMailchimpStatus()) return
let mailchimpInitialized = false
// Note: fails here should not block registration at any cost
try {
const config = getMailchimpConfig() // Note: throws an error if not configured
function initializeMailchimp() {
if (mailchimpInitialized) return
const config = getMailchimpConfig() // Note: throws an error if not configured
if (!config) throw new Error('Cannot initialize mailchimp without config values')
mailchimp.setConfig({
apiKey: config.apiKey,
server: config.serverPrefix
})
const user = await getUserById({ userId })
if (!user) {
throw new Error(
'Could not register user for newsletter - no db user record found.'
)
}
const [first, second] = user.name.split(' ')
const subscriberHash = md5(user.email.toLowerCase())
// NOTE: using setListMember (NOT addListMember) to prevent errors for previously
// registered members.
await mailchimp.lists.setListMember(config.listId, subscriberHash, {
status_if_new: 'subscribed',
email_address: user.email,
merge_fields: {
EMAIL: user.email,
FNAME: first,
LNAME: second,
FULLNAME: user.name // NOTE: this field needs to be set in the audience merge fields
}
})
} catch (e) {
logger.warn(e, 'Failed to register user to newsletter.')
}
mailchimp.setConfig({
apiKey: config.apiKey,
server: config.serverPrefix
})
mailchimpInitialized = true
}
export { addToMailchimpAudience }
async function addToMailchimpAudience(user: UserRecord, listId: string) {
initializeMailchimp()
// Do not do anything (inc. logging) if we do not explicitly enable it
// Note: fails here should not block registration at any cost
const [first, second] = user.name.split(' ')
const subscriberHash = md5(user.email.toLowerCase())
// NOTE: using setListMember (NOT addListMember) to prevent errors for previously
// registered members.
await mailchimp.lists.setListMember(listId, subscriberHash, {
status_if_new: 'subscribed',
email_address: user.email,
merge_fields: {
EMAIL: user.email,
FNAME: first,
LNAME: second,
FULLNAME: user.name // NOTE: this field needs to be set in the audience merge fields
}
})
}
async function triggerMailchimpCustomerJourney(
user: UserRecord,
{
listId,
journeyId,
stepId
}: {
listId: string
journeyId: number
stepId: number
}
) {
await addToMailchimpAudience(user, listId)
// @ts-expect-error the mailchimp api typing sucks
await mailchimp.customerJourneys.trigger(journeyId, stepId, {
email_address: user.email
})
}
export { addToMailchimpAudience, triggerMailchimpCustomerJourney }

Просмотреть файл

@ -6,12 +6,21 @@ const passport = require('passport')
const sentry = require('@/logging/sentryHelper')
const { createAuthorizationCode } = require('./services/apps')
const { getFrontendOrigin } = require('@/modules/shared/helpers/envHelper')
const {
getFrontendOrigin,
getMailchimpStatus,
getMailchimpNewsletterIds,
getMailchimpOnboardingIds
} = require('@/modules/shared/helpers/envHelper')
const { isSSLServer, getRedisUrl } = require('@/modules/shared/helpers/envHelper')
const { authLogger } = require('@/logging/logging')
const { authLogger, logger } = require('@/logging/logging')
const { createRedisClient } = require('@/modules/shared/redis/redis')
const { mixpanel } = require('@/modules/shared/utils/mixpanel')
const { addToMailchimpAudience } = require('./services/mailchimp')
const { mixpanel, resolveMixpanelUserId } = require('@/modules/shared/utils/mixpanel')
const {
addToMailchimpAudience,
triggerMailchimpCustomerJourney
} = require('./services/mailchimp')
const { getUserById } = require('@/modules/core/services/users')
/**
* TODO: Get rid of session entirely, we don't use it for the app and it's not really necessary for the auth flow, so it only complicates things
* NOTE: it does seem used!
@ -83,16 +92,34 @@ module.exports = async (app) => {
urlObj.searchParams.set('register', 'true')
// Send event to MP
const mixpanelUserId = req.user.email
? resolveMixpanelUserId(req.user.email)
: null
const isInvite = !!req.user.isInvite
if (req.user.email) {
await mixpanel({ userEmail: req.user.email }).track('Sign Up', {
if (mixpanelUserId) {
await mixpanel({ mixpanelUserId }).track('Sign Up', {
isInvite
})
}
}
if (newsletterConsent) {
await addToMailchimpAudience(req.user.id)
if (getMailchimpStatus()) {
try {
const user = await getUserById({ userId: req.user.id })
if (!user)
throw new Error(
'Could not register user for mailchimp lists - no db user record found.'
)
const onboardingIds = getMailchimpOnboardingIds()
await triggerMailchimpCustomerJourney(user, onboardingIds)
if (newsletterConsent) {
const { listId } = getMailchimpNewsletterIds()
await addToMailchimpAudience(user, listId)
}
} catch (error) {
logger.warn(error, 'Failed to sign up user to mailchimp lists')
}
}
}
const redirectUrl = urlObj.toString()

Просмотреть файл

@ -17,7 +17,7 @@ const {
} = require('../services/apps')
const { Scopes } = require('@/modules/core/helpers/mainConstants')
const { updateDefaultApp } = require('@/modules/auth/defaultApps')
const { updateDefaultApp } = require('@/modules/auth/manageDefaultApps')
const knex = require('@/db/knex')
const cryptoRandomString = require('crypto-random-string')

Просмотреть файл

@ -140,7 +140,7 @@ module.exports = {
/**
* @param {{userId: string}} param0
* @returns {import('@/modules/core/helpers/userHelper').UserRecord | null}
* @returns Promise<{import('@/modules/core/helpers/userHelper').UserRecord | null>}
*/
async getUserById({ userId }) {
const user = await Users().where({ id: userId }).select('*').first()

Просмотреть файл

@ -93,21 +93,35 @@ export function getMailchimpStatus() {
}
export function getMailchimpConfig() {
if (
!process.env.MAILCHIMP_API_KEY ||
!process.env.MAILCHIMP_SERVER_PREFIX ||
!process.env.MAILCHIMP_LIST_ID
) {
throw new MisconfiguredEnvironmentError('Mailchimp is not configured')
}
if (!getMailchimpStatus()) return null
if (!process.env.MAILCHIMP_API_KEY || !process.env.MAILCHIMP_SERVER_PREFIX)
throw new MisconfiguredEnvironmentError('Mailchimp api is not configured')
return {
apiKey: process.env.MAILCHIMP_API_KEY,
serverPrefix: process.env.MAILCHIMP_SERVER_PREFIX,
listId: process.env.MAILCHIMP_LIST_ID
serverPrefix: process.env.MAILCHIMP_SERVER_PREFIX
}
}
export function getMailchimpOnboardingIds() {
if (
!process.env.MAILCHIMP_ONBOARDING_LIST_ID ||
!process.env.MAILCHIMP_ONBOARDING_JOURNEY_ID ||
!process.env.MAILCHIMP_ONBOARDING_STEP_ID
)
throw new MisconfiguredEnvironmentError('Mailchimp onboarding is not configured')
return {
listId: process.env.MAILCHIMP_ONBOARDING_LIST_ID,
journeyId: parseInt(process.env.MAILCHIMP_ONBOARDING_JOURNEY_ID),
stepId: parseInt(process.env.MAILCHIMP_ONBOARDING_STEP_ID)
}
}
export function getMailchimpNewsletterIds() {
if (!process.env.MAILCHIMP_NEWSLETTER_LIST_ID)
throw new MisconfiguredEnvironmentError('Mailchimp newsletter id is not configured')
return { listId: process.env.MAILCHIMP_NEWSLETTER_LIST_ID }
}
/**
* Get app base url / canonical url / origin
* TODO: Go over all getBaseUrl() usages and move them to getXOrigin() instead
@ -197,8 +211,7 @@ export function enableMixpanel() {
}
export function speckleAutomateUrl() {
const automateUrl =
process.env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.systems'
const automateUrl = process.env.SPECKLE_AUTOMATE_URL
return automateUrl
}

Просмотреть файл

@ -76,3 +76,42 @@ export const WithManualClose: StoryType = {
}
}
}
export const NoCtaOrDescription: StoryObj = {
render: (args) => ({
components: { ToastRenderer, FormButton },
setup() {
const notification = ref(null as Nullable<ToastNotification>)
const onClick = () => {
// Update notification without cta or description
notification.value = {
type: ToastNotificationType.Info,
title: 'Displays a toast notification'
}
// Clear after 2s
setTimeout(() => (notification.value = null), 2000)
}
return { args, onClick, notification }
},
template: `
<div>
<FormButton @click="onClick">Trigger Title Only</FormButton>
<ToastRenderer v-model:notification="notification"/>
</div>
`
}),
parameters: {
docs: {
description: {
story: 'Displays a toast notification with only a title, no description or CTA.'
},
source: {
code: `
<FormButton @click="onClick">Trigger Title Only</FormButton>
<ToastRenderer v-model:notification="notification"/>
`
}
}
}
}

Просмотреть файл

@ -3,7 +3,7 @@
aria-live="assertive"
class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 mt-10 sm:items-start sm:p-6 z-50"
>
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<div class="flex w-full flex-col items-center gap-4 sm:items-end">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<Transition
enter-active-class="transform ease-out duration-300 transition"
@ -15,67 +15,72 @@
>
<div
v-if="notification"
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-foundation text-foreground shadow-lg ring-1 ring-primary-muted ring-opacity-5"
class="pointer-events-auto w-full max-w-[20rem] overflow-hidden rounded-lg bg-foundation text-foreground shadow-lg ring-1 ring-primary-muted ring-opacity-5"
:class="isTitleOnly ? 'p-2' : 'p-3'"
>
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<CheckCircleIcon
v-if="notification.type === ToastNotificationType.Success"
class="h-6 w-6 text-success"
aria-hidden="true"
/>
<XCircleIcon
v-else-if="notification.type === ToastNotificationType.Danger"
class="h-6 w-6 text-danger"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-else-if="notification.type === ToastNotificationType.Warning"
class="h-6 w-6 text-warning"
aria-hidden="true"
/>
<InformationCircleIcon
v-else-if="notification.type === ToastNotificationType.Info"
class="h-6 w-6 text-info"
aria-hidden="true"
/>
</div>
<div class="ml-2 w-0 flex-1 flex flex-col">
<p v-if="notification.title" class="text-foreground font-bold">
{{ notification.title }}
</p>
<p
v-if="notification.description"
class="label label--light text-foreground-2"
>
{{ notification.description }}
</p>
<div v-if="notification.cta" class="flex justify-start mt-2">
<CommonTextLink
:to="notification.cta.url"
class="label"
primary
@click="onCtaClick"
>
{{ notification.cta.title }}
</CommonTextLink>
</div>
</div>
<div
class="ml-4 flex flex-shrink-0"
:class="{ 'self-center': shouldVerticallyCenterCloser }"
<div class="flex gap-2" :class="isTitleOnly ? 'items-center' : 'items-start'">
<div class="flex-shrink-0">
<CheckCircleIcon
v-if="notification.type === ToastNotificationType.Success"
class="text-success"
:class="isTitleOnly ? 'h-5 w-5' : 'h-6 w-6'"
aria-hidden="true"
/>
<XCircleIcon
v-else-if="notification.type === ToastNotificationType.Danger"
class="text-danger"
:class="isTitleOnly ? 'h-5 w-5' : 'h-6 w-6'"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-else-if="notification.type === ToastNotificationType.Warning"
class="text-warning"
:class="isTitleOnly ? 'h-5 w-5' : 'h-6 w-6'"
aria-hidden="true"
/>
<InformationCircleIcon
v-else-if="notification.type === ToastNotificationType.Info"
class="text-info"
:class="isTitleOnly ? 'h-5 w-5' : 'h-6 w-6'"
aria-hidden="true"
/>
</div>
<div class="w-full min-w-[10rem]">
<p
v-if="notification.title"
class="text-foreground font-bold"
:class="isTitleOnly ? 'text-sm' : 'text-base'"
>
<button
type="button"
class="inline-flex rounded-md bg-foundation text-foreground-2 hover:text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
@click="dismiss"
{{ notification.title }}
</p>
<p
v-if="notification.description"
class="label label--light text-foreground-2 text-sm"
>
{{ notification.description }}
</p>
<div v-if="notification.cta">
<CommonTextLink
:to="notification.cta.url"
class="label"
size="xs"
primary
@click="onCtaClick"
>
<span class="sr-only">Close</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
{{ notification.cta.title }}
</CommonTextLink>
</div>
</div>
<div class="ml-2 flex-shrink-0" :class="isTitleOnly ? 'mt-1.5' : ''">
<button
type="button"
class="inline-flex rounded-md bg-foundation text-foreground-2 hover:text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
@click="dismiss"
>
<span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</Transition>
@ -104,7 +109,7 @@ const props = defineProps<{
notification: Nullable<ToastNotification>
}>()
const shouldVerticallyCenterCloser = computed(
const isTitleOnly = computed(
() => !props.notification?.description && !props.notification?.cta
)

Просмотреть файл

@ -0,0 +1,68 @@
import {
CameraController,
Extension,
GeometryType,
IViewer,
MeshBatch,
Vector3
} from '@speckle/viewer'
import { PerspectiveCamera } from 'three'
export class CameraPlanes extends Extension {
private camerController: CameraController
public constructor(viewer: IViewer) {
super(viewer)
this.camerController = viewer.getExtension(CameraController)
}
public onEarlyUpdate(): void {
this.computePerspectiveCameraPlanes()
}
public computePerspectiveCameraPlanes() {
const camera = this.viewer.getRenderer().renderingCamera as PerspectiveCamera
const minDist = this.getClosestGeometryDistance(camera)
if (minDist === Number.POSITIVE_INFINITY) return
const fov = camera.fov
const aspect = camera.aspect
const nearPlane =
Math.max(minDist, 0) /
Math.sqrt(
1 +
Math.pow(Math.tan(((fov / 180) * Math.PI) / 2), 2) * (Math.pow(aspect, 2) + 1)
)
this.viewer.getRenderer().renderingCamera.near = nearPlane
console.log(minDist, nearPlane)
}
public getClosestGeometryDistance(camera: PerspectiveCamera): number {
const cameraPosition = camera.position
const cameraTarget = this.camerController.controls.getTarget(new Vector3())
const cameraDir = new Vector3()
.subVectors(cameraTarget, camera.position)
.normalize()
const batches = this.viewer
.getRenderer()
.batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[]
let minDist = Number.POSITIVE_INFINITY
const minPoint = new Vector3()
for (let b = 0; b < batches.length; b++) {
const result = batches[b].mesh.TAS.closestPointToPoint(cameraPosition)
const planarity = cameraDir.dot(
new Vector3().subVectors(result.point, cameraPosition).normalize()
)
if (planarity > 0) {
const dist = cameraPosition.distanceTo(result.point)
if (dist < minDist) {
minDist = dist
minPoint.copy(result.point)
}
}
}
return minDist
}
}

Просмотреть файл

@ -455,7 +455,7 @@ export default class Sandbox {
title: 'Screenshot'
})
screenshot.on('click', async () => {
// console.warn(await this.viewer.screenshot())
console.warn(await this.viewer.screenshot())
// const start = performance.now()
// const nodes = this.viewer.getWorldTree().root.all(
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -465,10 +465,6 @@ export default class Sandbox {
// this.viewer.cancelLoad(
// 'https://latest.speckle.dev/streams/97750296c2/objects/c3138e24a866d447eb86b2a8107b2c09'
// )
this.viewer
.getExtension(FilteringExtension)
.isolateObjects(this.ids /*['1c8f29e7d48e531f6acbf987a50467f9']*/)
})
const rotate = this.tabs.pages[0].addButton({

Просмотреть файл

@ -347,16 +347,23 @@ const getStream = () => {
// 'https://latest.speckle.dev/streams/97750296c2/objects/11a7752e40b4ef0620affc55ce9fdf5a'
// 'https://latest.speckle.dev/streams/92b620fb17/objects/7118603b197c00944f53be650ce721ec'
// Blender Mega Test Stream
// 'https://latest.speckle.dev/streams/c1faab5c62/commits/2ecb757577'
// 'https://latest.speckle.dev/streams/c1faab5c62/commits/3deaea94af'
// Text and Dimensions
// 'https://latest.speckle.dev/streams/3f895e614f/commits/fbc78286c9'
// 'https://latest.speckle.dev/streams/55cc1cbf0a/commits/aa72674507'
// 'https://latest.speckle.dev/streams/55cc1cbf0a/commits/a7f74b6524'
// 'https://latest.speckle.dev/streams/85e05b8c72/commits/53f4328211'
// 'https://latest.speckle.dev/streams/aea12cab71/commits/787ade768e'
// 'https://latest.speckle.dev/streams/e9285828d7/commits/9b80b7a70c'
// 'https://speckle.xyz/streams/b85d53c3b4/commits/be26146460'
// Germany
// 'https://latest.speckle.dev/streams/7117052f4e/commits/a646bf659e'
// 'https://latest.speckle.dev/streams/aea12cab71/commits/787ade768e'
// 'https://speckle.xyz/streams/a29e5c7772/commits/a8cfae2645'
)
}

Просмотреть файл

@ -59,6 +59,7 @@ import { SpeckleWebGLRenderer } from './objects/SpeckleWebGLRenderer'
import { SpeckleTypeAllRenderables } from './loaders/GeometryConverter'
import SpeckleInstancedMesh from './objects/SpeckleInstancedMesh'
import { BaseSpecklePass } from './pipeline/SpecklePass'
import { CameraController } from './extensions/core-extensions/CameraController'
export class RenderingStats {
private renderTimeAcc = 0
@ -236,6 +237,9 @@ export default class SpeckleRenderer {
this.pipeline.reset()
}
})
this.cameraProvider.on(CameraControllerEvent.ProjectionChanged, () => {
;(this._cameraProvider as CameraController).setCameraPlanes(this.sceneBox)
})
}
public get renderingCamera() {
@ -610,6 +614,7 @@ export default class SpeckleRenderer {
/** We'll just update the shadowcatcher after all batches are loaded */
this.updateShadowCatcher()
this.updateClippingPlanes()
;(this._cameraProvider as CameraController).setCameraPlanes(this.sceneBox)
delete this.cancel[subtreeId]
}

Просмотреть файл

@ -26,6 +26,7 @@ import Logger from 'js-logger'
import { ObjectLayers } from '../../IViewer'
import { DrawGroup } from './InstancedMeshBatch'
import Materials from '../materials/Materials'
import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial'
export default class MeshBatch implements Batch {
public id: string
@ -247,6 +248,16 @@ export default class MeshBatch implements Batch {
minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex)
maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex)
}
/** We need to update the texture here, because each batch uses it's own clone for any material we use on it
* because otherwise three.js won't properly update our custom uniforms
*/
if (range[k].materialOptions.rampTexture !== undefined) {
if (range[k].material instanceof SpeckleStandardColoredMaterial) {
;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture(
range[k].materialOptions.rampTexture
)
}
}
}
}
if (minGradientIndex < Infinity && maxGradientIndex > 0)
@ -485,7 +496,7 @@ export default class MeshBatch implements Batch {
})
.slice()
this.geometry.groups.sort((a, b) => {
groups.sort((a, b) => {
const materialA: Material = (this.mesh.material as Array<Material>)[
a.materialIndex
]

Просмотреть файл

@ -2,7 +2,7 @@ import * as THREE from 'three'
import CameraControls from 'camera-controls'
import { Extension } from './Extension'
import { SpeckleCameraControls } from '../../objects/SpeckleCameraControls'
import { Box3, OrthographicCamera, PerspectiveCamera, Vector3 } from 'three'
import { Box3, OrthographicCamera, PerspectiveCamera, Sphere, Vector3 } from 'three'
import { KeyboardKeyHold, HOLD_EVENT_TYPE } from 'hold-event'
import { CanonicalView, SpeckleView, InlineView, IViewer } from '../../..'
import {
@ -11,6 +11,7 @@ import {
ICameraProvider,
PolarView
} from './Providers'
import Logger from 'js-logger'
export class CameraController extends Extension implements ICameraProvider {
get provide() {
@ -146,9 +147,9 @@ export class CameraController extends Extension implements ICameraProvider {
this.viewer.getContainer().offsetWidth / this.viewer.getContainer().offsetHeight
this.perspectiveCamera.updateProjectionMatrix()
const lineOfSight = new THREE.Vector3()
const lineOfSight = new Vector3()
this.perspectiveCamera.getWorldDirection(lineOfSight)
const target = new THREE.Vector3()
const target = new Vector3()
this._controls.getTarget(target)
const distance = target.clone().sub(this.perspectiveCamera.position)
const depth = distance.dot(lineOfSight)
@ -191,9 +192,9 @@ export class CameraController extends Extension implements ICameraProvider {
protected setupOrthoCamera() {
this._controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM
const lineOfSight = new THREE.Vector3()
const lineOfSight = new Vector3()
this.perspectiveCamera.getWorldDirection(lineOfSight)
const target = new THREE.Vector3().copy(this.viewer.World.worldOrigin)
const target = new Vector3().copy(this.viewer.World.worldOrigin)
const distance = target.clone().sub(this.perspectiveCamera.position)
const depth = distance.length()
const dims = {
@ -346,6 +347,30 @@ export class CameraController extends Extension implements ICameraProvider {
)
}
public setCameraPlanes(targetVolume: Box3, offsetScale: number = 1) {
if (targetVolume.isEmpty()) {
Logger.error('Cannot set camera planes for empty volume')
return
}
const size = targetVolume.getSize(new Vector3())
const maxSize = Math.max(size.x, size.y, size.z)
const camFov =
this._renderingCamera === this.perspectiveCamera ? this.fieldOfView : 55
const camAspect =
this._renderingCamera === this.perspectiveCamera ? this.aspect : 1.2
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camFov) / 360))
const fitWidthDistance = fitHeightDistance / camAspect
const distance = offsetScale * Math.max(fitHeightDistance, fitWidthDistance)
this._controls.minDistance = distance / 100
this._controls.maxDistance = distance * 100
this._renderingCamera.near = distance / 100
this._renderingCamera.far = distance * 100
this._renderingCamera.updateProjectionMatrix()
}
protected zoom(objectIds?: string[], fit?: number, transition?: boolean) {
if (!objectIds) {
this.zoomExtents(fit, transition)
@ -356,7 +381,7 @@ export class CameraController extends Extension implements ICameraProvider {
private zoomExtents(fit = 1.2, transition = true) {
if (this.viewer.getRenderer().clippingVolume.isEmpty()) {
const box = new THREE.Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
const box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
this.zoomToBox(box, fit, transition)
return
}
@ -376,36 +401,17 @@ export class CameraController extends Extension implements ICameraProvider {
private zoomToBox(box, fit = 1.2, transition = true) {
if (box.max.x === Infinity || box.max.x === -Infinity) {
box = new THREE.Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
}
const fitOffset = fit
const size = box.getSize(new Vector3())
const target = new THREE.Sphere()
const target = new Sphere()
box.getBoundingSphere(target)
target.radius = target.radius * fitOffset
const maxSize = Math.max(size.x, size.y, size.z)
const camFov =
this._renderingCamera === this.perspectiveCamera ? this.fieldOfView : 55
const camAspect =
this._renderingCamera === this.perspectiveCamera ? this.aspect : 1.2
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camFov) / 360))
const fitWidthDistance = fitHeightDistance / camAspect
const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance)
target.radius = target.radius * fit
this._controls.fitToSphere(target, transition)
this._controls.minDistance = distance / 100
this._controls.maxDistance = distance * 100
this._renderingCamera.near = Math.max(distance / 100, 0.1)
this._renderingCamera.far = distance * 100
this._renderingCamera.updateProjectionMatrix()
this.setCameraPlanes(box, fit)
if (this._renderingCamera === this.orthographicCamera) {
this._renderingCamera.far = distance * 100
this._renderingCamera.updateProjectionMatrix()
// fit the camera inside, so we don't have clipping plane issues.
// WIP implementation
const camPos = this._renderingCamera.position
@ -537,10 +543,10 @@ export class CameraController extends Extension implements ICameraProvider {
default: {
let box
if (this.viewer.getRenderer().allObjects.children.length === 0)
box = new THREE.Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
else box = new THREE.Box3().setFromObject(this.viewer.getRenderer().allObjects)
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
else box = new Box3().setFromObject(this.viewer.getRenderer().allObjects)
if (box.max.x === Infinity || box.max.x === -Infinity) {
box = new THREE.Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1))
}
this._controls.setPosition(box.max.x, box.max.y, box.max.z, transition)
this.zoomExtents()

Просмотреть файл

@ -278,6 +278,10 @@ export class TopLevelAccelerationStructure {
return ret
}
public closestPointToPoint(point: Vector3) {
return this.accelerationStructure.bvh.closestPointToPoint(point)
}
public getBoundingBox(target: Box3): Box3 {
this.accelerationStructure.getBoundingBox(target)
return target

Просмотреть файл

@ -44,7 +44,7 @@ spec:
livenessProbe:
httpGet:
path: /health
path: /api/status
port: www
failureThreshold: 3
initialDelaySeconds: 10
@ -53,7 +53,7 @@ spec:
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
path: /api/status
port: www
failureThreshold: 1
initialDelaySeconds: 5

Просмотреть файл

@ -20,7 +20,7 @@ spec:
spec:
containers:
- name: main
image: speckle/speckle-server:{{ .Values.docker_image_tag }}
image: {{ default (printf "speckle/speckle-server:%s" .Values.docker_image_tag) .Values.objects.image }}
imagePullPolicy: {{ .Values.imagePullPolicy }}
args: #overwrites the Dockerfile CMD statement
{{- if .Values.objects.inspect.enabled }}
@ -285,26 +285,9 @@ spec:
value: "{{ .Values.server.email.from }}"
{{- end }}
# *** Newsletter ***
{{- if (default false (.Values.server.mailchimp).enabled) }}
# *** Newsletter will not be generated by objects pods ***
- name: MAILCHIMP_ENABLED
value: "true"
- name: MAILCHIMP_API_KEY
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).apikey).secretName }}
key: {{ default "mailchimp_apikey" ((.Values.server.mailchimp).apikey).secretKey }}
- name: MAILCHIMP_SERVER_PREFIX
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).serverprefix).secretName }}
key: {{ default "mailchimp_serverprefix" ((.Values.server.mailchimp).serverprefix).secretKey }}
- name: MAILCHIMP_LIST_ID
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).listid).secretName }}
key: {{ default "mailchimp_listid" ((.Values.server.mailchimp).listid).secretKey }}
{{- end }}
value: "false"
# *** Tracking / Tracing ***
- name: SENTRY_DSN

Просмотреть файл

@ -26,4 +26,17 @@ spec:
name: speckle-frontend
port:
name: www
{{- end }}
- pathType: Exact
path: "/api/status"
backend:
service:
{{- if .Values.frontend_2.enabled }}
name: speckle-frontend-2
port:
name: web
{{- else }}
name: speckle-frontend
port:
name: www
{{- end }}

Просмотреть файл

@ -302,24 +302,28 @@ spec:
{{- end }}
# *** Newsletter ***
{{- if (default false (.Values.server.mailchimp).enabled) }}
{{- if .Values.server.mailchimp.enabled }}
- name: MAILCHIMP_ENABLED
value: "true"
- name: MAILCHIMP_API_KEY
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).apikey).secretName }}
key: {{ default "mailchimp_apikey" ((.Values.server.mailchimp).apikey).secretKey }}
name: {{ default .Values.secretName .Values.server.mailchimp.apikey.secretName }}
key: {{ .Values.server.mailchimp.apikey.secretKey }}
- name: MAILCHIMP_SERVER_PREFIX
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).serverprefix).secretName }}
key: {{ default "mailchimp_serverprefix" ((.Values.server.mailchimp).serverprefix).secretKey }}
- name: MAILCHIMP_LIST_ID
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName ((.Values.server.mailchimp).listid).secretName }}
key: {{ default "mailchimp_listid" ((.Values.server.mailchimp).listid).secretKey }}
value: "{{ .Values.server.mailchimp.serverPrefix}}"
- name: MAILCHIMP_NEWSLETTER_LIST_ID
value: "{{ .Values.server.mailchimp.newsletterListId}}"
- name: MAILCHIMP_ONBOARDING_LIST_ID
value: "{{ .Values.server.mailchimp.onboardingListId}}"
- name: MAILCHIMP_ONBOARDING_JOURNEY_ID
value: "{{ .Values.server.mailchimp.onboardingJourneyId}}"
- name: MAILCHIMP_ONBOARDING_STEP_ID
value: "{{ .Values.server.mailchimp.onboardingStepId}}"
{{- end }}
# *** Tracking / Tracing ***

Просмотреть файл

@ -983,6 +983,56 @@
}
}
},
"mailchimp": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Mailchimp integration feature flag",
"default": false
},
"apikey": {
"type": "object",
"properties": {
"secretName": {
"type": "string",
"description": "The name of the Kubernetes Secret containing the Mailchimp API key.",
"default": ""
},
"secretKey": {
"type": "string",
"description": "The key within the Kubernetes Secret holding the Mailchimp API key as its value.",
"default": "mailchimp_apikey"
}
}
},
"serverPrefix": {
"type": "string",
"description": "Mailchimp api server prefix",
"default": ""
},
"newsletterListId": {
"type": "string",
"description": "Audience id for the newsletter list",
"default": ""
},
"onboardingListId": {
"type": "string",
"description": "Audience id for the onboarding list",
"default": ""
},
"onboardingJourneyId": {
"type": "string",
"description": "Id of the onboarding journey",
"default": ""
},
"onboardingStepId": {
"type": "string",
"description": "Id of the onboarding journey step we trigger",
"default": ""
}
}
},
"migration": {
"type": "object",
"properties": {
@ -1095,6 +1145,11 @@
"description": "The number of instances of the Server pod to be deployed within the cluster.",
"default": 1
},
"image": {
"type": "string",
"description": "The Docker image to be used for the Speckle Objects component. If blank, defaults to speckle/speckle-server:{{ .Values.docker_image_tag }}. If provided, this value should be the full path including tag. The docker_image_tag value will be ignored.",
"default": ""
},
"logLevel": {
"type": "string",
"description": "The minimum level of logs which will be output. Suitable values are trace, debug, info, warn, error, fatal, or silent",

Просмотреть файл

@ -647,6 +647,25 @@ server:
## @param server.fileUploads.enabled If enabled, file uploads on the server will be flagged as enabled
enabled: true
mailchimp:
## @param server.mailchimp.enabled Mailchimp integration feature flag
enabled: false
apikey:
## @param server.mailchimp.apikey.secretName The name of the Kubernetes Secret containing the Mailchimp API key.
secretName: '' # defaults to .secretName
## @param server.mailchimp.apikey.secretKey The key within the Kubernetes Secret holding the Mailchimp API key as its value.
secretKey: 'mailchimp_apikey'
## @param server.mailchimp.serverPrefix Mailchimp api server prefix
serverPrefix: ''
## @param server.mailchimp.newsletterListId Audience id for the newsletter list
newsletterListId: ''
## @param server.mailchimp.onboardingListId Audience id for the onboarding list
onboardingListId: ''
## @param server.mailchimp.onboardingJourneyId Id of the onboarding journey
onboardingJourneyId: ''
## @param server.mailchimp.onboardingStepId Id of the onboarding journey step we trigger
onboardingStepId: ''
migration:
## @param server.migration.movedFrom Indicate the URL where the server moved from
movedFrom: ''
@ -676,6 +695,7 @@ server:
## @param server.sentry_dns (Optional) The Data Source Name that was provided by Sentry.io
## Sentry.io allows events within Speckle to be monitored
##
sentry_dns: ''
## @param server.disable_tracking If set to true, will prevent tracking metrics from being collected
## Setting this value to false requires `sentry_dns` to be set
@ -717,6 +737,9 @@ objects:
## @param objects.replicas The number of instances of the Server pod to be deployed within the cluster.
##
replicas: 1
## @param objects.image The Docker image to be used for the Speckle Objects component. If blank, defaults to speckle/speckle-server:{{ .Values.docker_image_tag }}. If provided, this value should be the full path including tag. The docker_image_tag value will be ignored.
##
image: ''
## @param objects.logLevel The minimum level of logs which will be output. Suitable values are trace, debug, info, warn, error, fatal, or silent
##
logLevel: 'info'

Просмотреть файл

@ -13902,7 +13902,7 @@ __metadata:
vee-validate: ^4.7.0
vue-advanced-cropper: ^2.8.8
vue-tippy: ^6.0.0
vue-tsc: 1.8.22
vue-tsc: 1.8.27
wait-on: ^6.0.1
ws: ^8.9.0
languageName: unknown
@ -18241,6 +18241,15 @@ __metadata:
languageName: node
linkType: hard
"@volar/language-core@npm:1.11.1, @volar/language-core@npm:~1.11.1":
version: 1.11.1
resolution: "@volar/language-core@npm:1.11.1"
dependencies:
"@volar/source-map": 1.11.1
checksum: 7f98fbeb96ff1093dbaa47e790575a98d1fd2103d9bb1598ec7b0ae787fc6af2ffcea12fdea0f0a4e057f38f6ee3a60bd54f2af3985159319021771f79df9451
languageName: node
linkType: hard
"@volar/language-core@npm:1.4.0-alpha.4":
version: 1.4.0-alpha.4
resolution: "@volar/language-core@npm:1.4.0-alpha.4"
@ -18268,6 +18277,15 @@ __metadata:
languageName: node
linkType: hard
"@volar/source-map@npm:1.11.1, @volar/source-map@npm:~1.11.1":
version: 1.11.1
resolution: "@volar/source-map@npm:1.11.1"
dependencies:
muggle-string: ^0.3.1
checksum: 1ec1034432ee51a0afe187ba9158292dd607a90d01120ee8a36cf27f5d464da5282c8fe7b0de82f52f45474a840c63eba666254c5c21ca5466dc02d0c95cd147
languageName: node
linkType: hard
"@volar/source-map@npm:1.4.0-alpha.4":
version: 1.4.0-alpha.4
resolution: "@volar/source-map@npm:1.4.0-alpha.4"
@ -18305,6 +18323,16 @@ __metadata:
languageName: node
linkType: hard
"@volar/typescript@npm:~1.11.1":
version: 1.11.1
resolution: "@volar/typescript@npm:1.11.1"
dependencies:
"@volar/language-core": 1.11.1
path-browserify: ^1.0.1
checksum: 0db2fc32db133e493f05dbafd248560a6d4e5b071a0d80422c67b1875bd36980c113915d876a83e855d55c2880b2e7b9f04f803ce3504a4d6fafcc0b801c621b
languageName: node
linkType: hard
"@volar/vue-language-core@npm:1.3.4":
version: 1.3.4
resolution: "@volar/vue-language-core@npm:1.3.4"
@ -19105,6 +19133,28 @@ __metadata:
languageName: node
linkType: hard
"@vue/language-core@npm:1.8.27":
version: 1.8.27
resolution: "@vue/language-core@npm:1.8.27"
dependencies:
"@volar/language-core": ~1.11.1
"@volar/source-map": ~1.11.1
"@vue/compiler-dom": ^3.3.0
"@vue/shared": ^3.3.0
computeds: ^0.0.1
minimatch: ^9.0.3
muggle-string: ^0.3.1
path-browserify: ^1.0.1
vue-template-compiler: ^2.7.14
peerDependencies:
typescript: "*"
peerDependenciesMeta:
typescript:
optional: true
checksum: 8660c05319be8dc5daacc2cd929171434215d29f3ad5bfbe0038d1967db05b8bf640286b25f338845cc1e3890b4aaa239ac9e8cb832cc8a50a5bbdff31b2edd1
languageName: node
linkType: hard
"@vue/language-core@npm:1.8.8":
version: 1.8.8
resolution: "@vue/language-core@npm:1.8.8"
@ -41540,8 +41590,8 @@ __metadata:
linkType: hard
"sanitize-html@npm:^2.7.1":
version: 2.8.1
resolution: "sanitize-html@npm:2.8.1"
version: 2.12.1
resolution: "sanitize-html@npm:2.12.1"
dependencies:
deepmerge: ^4.2.2
escape-string-regexp: ^4.0.0
@ -41549,7 +41599,7 @@ __metadata:
is-plain-object: ^5.0.0
parse-srcset: ^1.0.2
postcss: ^8.3.11
checksum: 0d35503b261800b736a02648e8b9b2a5206cbc621248cf8dd86d5b9bdd470d0146d74704222d287bd7359a599e8a186cc5b015401237b0244352f18f37465daa
checksum: fb96ea7170d51b5af2607f5cfd84464c78fc6f47e339407f55783e781c6a0288a8d40bbf97ea6a8758924ba9b2d33dcc4846bb94caacacd90d7f2de10ed8541a
languageName: node
linkType: hard
@ -46632,7 +46682,22 @@ __metadata:
languageName: node
linkType: hard
"vue-tsc@npm:1.8.22, vue-tsc@npm:^1.8.20, vue-tsc@npm:^1.8.22":
"vue-tsc@npm:1.8.27":
version: 1.8.27
resolution: "vue-tsc@npm:1.8.27"
dependencies:
"@volar/typescript": ~1.11.1
"@vue/language-core": 1.8.27
semver: ^7.5.4
peerDependencies:
typescript: "*"
bin:
vue-tsc: bin/vue-tsc.js
checksum: 98c2986df01000a3245b5f08b9db35d0ead4f46fb12f4fe771257b4aa61aa4c26dda359aaa0e6c484a6240563d5188aaa6ed312dd37cc2315922d5e079260001
languageName: node
linkType: hard
"vue-tsc@npm:^1.8.20, vue-tsc@npm:^1.8.22":
version: 1.8.22
resolution: "vue-tsc@npm:1.8.22"
dependencies: