mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 17:41:53 +03:00
Improve Sentry support (#11776)
- Close https://github.com/enso-org/cloud-v2/issues/1590 - Add source maps to built bundle for sentry to pick up - Close https://github.com/enso-org/cloud-v2/issues/1589 - Add global Sentry filter to avoid sending 400 responses from backend - Wrap settings user and organization profile picture inputs in form - Upgrade from deprecated sentry APIs # Important Notes None
This commit is contained in:
parent
42cfa69c7e
commit
20c066ac65
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -701,10 +701,13 @@ jobs:
|
||||
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
|
||||
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
|
||||
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
|
||||
ENSO_CLOUD_SENTRY_ORGANIZATION: ${{ vars.ENSO_CLOUD_SENTRY_ORGANIZATION }}
|
||||
ENSO_CLOUD_SENTRY_PROJECT: ${{ vars.ENSO_CLOUD_SENTRY_PROJECT }}
|
||||
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
|
||||
ENSO_IDE_AG_GRID_LICENSE_KEY: ${{ vars.ENSO_AG_GRID_LICENSE_KEY }}
|
||||
ENSO_IDE_MAPBOX_API_TOKEN: ${{ vars.ENSO_MAPBOX_API_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.MICROSOFT_CODE_SIGNING_CERT_PASSWORD }}
|
||||
WIN_CSC_LINK: ${{ secrets.MICROSOFT_CODE_SIGNING_CERT }}
|
||||
- if: failure() && runner.os == 'Windows'
|
||||
|
3
app/gui/.gitignore
vendored
3
app/gui/.gitignore
vendored
@ -33,3 +33,6 @@ src/project-view/util/iconName.ts
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
@ -15,10 +15,13 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/enso-org/enso/issues"
|
||||
},
|
||||
"//": {},
|
||||
"//": [
|
||||
"--max-old-space-size=4096 is required when sourcemaps are enabled,",
|
||||
"otherwise Rollup runs out of memory when Vite is rendering chunks."
|
||||
],
|
||||
"scripts": {
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json",
|
||||
"build": "vite build",
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||
"build-cloud": "cross-env CLOUD_BUILD=true corepack pnpm run build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --cache --max-warnings=0",
|
||||
@ -41,6 +44,11 @@
|
||||
"playwright:install": "playwright install chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-grid-community/client-side-row-model": "^32.3.3",
|
||||
"@ag-grid-community/core": "^32.3.3",
|
||||
"@ag-grid-community/styles": "^32.3.3",
|
||||
"@ag-grid-enterprise/core": "^32.3.3",
|
||||
"@ag-grid-enterprise/range-selection": "^32.3.3",
|
||||
"@aws-amplify/auth": "5.6.5",
|
||||
"@aws-amplify/core": "5.8.5",
|
||||
"@hookform/resolvers": "^3.4.0",
|
||||
@ -48,24 +56,46 @@
|
||||
"@lexical/html": "^0.21.0",
|
||||
"@lexical/link": "^0.21.0",
|
||||
"@lexical/markdown": "^0.21.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@sentry/react": "^7.74.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@react-aria/interactions": "^3.22.3",
|
||||
"@sentry/react": "^7.74.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@stripe/react-stripe-js": "^2.7.1",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"@tanstack/react-query": "5.55.0",
|
||||
"@tanstack/vue-query": ">= 5.54.0 < 5.56.0",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"ag-grid-community": "^32.3.3",
|
||||
"ag-grid-enterprise": "^32.3.3",
|
||||
"ag-grid-vue3": "^32.3.3",
|
||||
"ajv": "^8.12.0",
|
||||
"amazon-cognito-identity-js": "6.3.6",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"clsx": "^2.1.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"culori": "^3.2.0",
|
||||
"enso-common": "workspace:*",
|
||||
"events": "^3.3.0",
|
||||
"framer-motion": "11.3.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"input-otp": "1.2.4",
|
||||
"install": "^0.13.0",
|
||||
"is-network-error": "^1.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"lexical": "^0.21.0",
|
||||
"lib0": "^0.2.85",
|
||||
"magic-string": "^0.30.3",
|
||||
"marked": "14.1.3",
|
||||
"monaco-editor": "0.48.0",
|
||||
"murmurhash": "^2.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"postcss-inline-svg": "^6.0.0",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"qrcode.react": "3.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-aria": "^3.34.3",
|
||||
@ -83,15 +113,7 @@
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"ts-results": "^3.3.0",
|
||||
"validator": "^13.12.0",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.4",
|
||||
"@ag-grid-community/client-side-row-model": "^32.3.3",
|
||||
"@ag-grid-community/core": "^32.3.3",
|
||||
"@ag-grid-community/styles": "^32.3.3",
|
||||
"@ag-grid-enterprise/core": "^32.3.3",
|
||||
"@ag-grid-enterprise/range-selection": "^32.3.3",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"@codemirror/commands": "^6.7.1",
|
||||
"@codemirror/language": "^6.10.6",
|
||||
"@codemirror/lang-markdown": "^v6.3.0",
|
||||
@ -101,25 +123,6 @@
|
||||
"@codemirror/view": "^6.35.3",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@floating-ui/vue": "^1.0.6",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"ag-grid-community": "^32.3.3",
|
||||
"ag-grid-enterprise": "^32.3.3",
|
||||
"ag-grid-vue3": "^32.3.3",
|
||||
"codemirror": "^6.0.1",
|
||||
"culori": "^3.2.0",
|
||||
"events": "^3.3.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"install": "^0.13.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"lib0": "^0.2.85",
|
||||
"magic-string": "^0.30.3",
|
||||
"murmurhash": "^2.0.1",
|
||||
"postcss-inline-svg": "^6.0.0",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"sucrase": "^3.34.0",
|
||||
"veaury": "^2.3.18",
|
||||
"vue": "^3.5.2",
|
||||
@ -129,14 +132,19 @@
|
||||
"y-websocket": "^1.5.0",
|
||||
"ydoc-shared": "workspace:*",
|
||||
"yjs": "^13.6.7",
|
||||
"marked": "14.1.3"
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@babel/plugin-syntax-import-attributes": "^7.24.7",
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@danmarshall/deckgl-typings": "^4.9.28",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@histoire/plugin-vue": "^0.17.12",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@open-rpc/server-js": "^1.9.4",
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@react-types/shared": "^3.22.1",
|
||||
"@storybook/addon-essentials": "^8.4.2",
|
||||
"@storybook/addon-interactions": "^8.4.2",
|
||||
@ -148,30 +156,10 @@
|
||||
"@storybook/vue3": "^8.4.2",
|
||||
"@storybook/vue3-vite": "^8.4.2",
|
||||
"@tanstack/react-query-devtools": "5.45.1",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/validator": "^13.11.7",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"fast-check": "^3.15.0",
|
||||
"playwright": "^1.39.0",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"tailwindcss-react-aria-components": "^1.1.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.4.10",
|
||||
"vitest": "^1.3.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@danmarshall/deckgl-typings": "^4.9.28",
|
||||
"@histoire/plugin-vue": "^0.17.12",
|
||||
"@open-rpc/server-js": "^1.9.4",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.0.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/css.escape": "^1.5.2",
|
||||
"@types/culori": "^2.0.1",
|
||||
@ -179,37 +167,53 @@
|
||||
"@types/hash-sum": "^1.0.0",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/mapbox-gl": "^3.4.1",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/shuffle-seed": "^1.1.0",
|
||||
"@types/tar": "^6.1.4",
|
||||
"@types/validator": "^13.11.7",
|
||||
"@types/wicg-file-system-access": "^2023.10.2",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitest/coverage-v8": "^1.3.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.0.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"chalk": "^5.3.0",
|
||||
"chromatic": "11.18.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css.escape": "^1.5.1",
|
||||
"d3": "^7.4.0",
|
||||
"enso-common": "workspace:*",
|
||||
"fast-check": "^3.15.0",
|
||||
"floating-vue": "^2.0.0-beta.24",
|
||||
"hash-wasm": "^4.11.0",
|
||||
"histoire": "^0.17.2",
|
||||
"jsdom": "^24.1.0",
|
||||
"playwright": "^1.39.0",
|
||||
"postcss": "^8.4.29",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"shuffle-seed": "^1.1.6",
|
||||
"sql-formatter": "^13.0.0",
|
||||
"storybook": "^8.4.2",
|
||||
"chromatic": "11.18.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"tailwindcss-react-aria-components": "^1.1.1",
|
||||
"tar": "^6.2.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-vue-devtools": "7.6.3",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"vitest": "^1.3.1",
|
||||
"vue-react-wrapper": "^0.3.1",
|
||||
"vue-tsc": "^2.0.24",
|
||||
"yaml": "^2.4.5",
|
||||
|
@ -0,0 +1,62 @@
|
||||
/** @file A hidden file input. */
|
||||
import {
|
||||
Form,
|
||||
type FieldPath,
|
||||
type FieldProps,
|
||||
type FieldStateProps,
|
||||
type FieldValues,
|
||||
type FieldVariantProps,
|
||||
type TSchema,
|
||||
} from '#/components/AriaComponents'
|
||||
import { Input, type InputProps } from '#/components/aria'
|
||||
|
||||
/** Props for {@link HiddenFile}. */
|
||||
export interface HiddenFileProps<Schema extends TSchema, TFieldName extends FieldPath<Schema>>
|
||||
extends FieldStateProps<
|
||||
Omit<InputProps, 'children' | 'value'> & { value: FieldValues<Schema>[TFieldName] },
|
||||
Schema,
|
||||
TFieldName
|
||||
>,
|
||||
FieldProps,
|
||||
FieldVariantProps {
|
||||
/** When true, triggers `form.submit()` on input. */
|
||||
readonly autoSubmit?: boolean | undefined
|
||||
readonly accept?: string | undefined
|
||||
}
|
||||
|
||||
/** A hidden file input. */
|
||||
export function HiddenFile<Schema extends TSchema, TFieldName extends FieldPath<Schema>>(
|
||||
props: HiddenFileProps<Schema, TFieldName>,
|
||||
) {
|
||||
const {
|
||||
form,
|
||||
autoSubmit = false,
|
||||
accept,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
defaultValue: _defaultValue,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
disabled: _disabled,
|
||||
...inputProps
|
||||
} = props
|
||||
const formInstance = Form.useFormContext(form)
|
||||
|
||||
return (
|
||||
<Form.Controller
|
||||
{...inputProps}
|
||||
control={formInstance.control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="file"
|
||||
className="focus-child w-0"
|
||||
accept={accept}
|
||||
onChange={(event) => {
|
||||
field.onChange(event.target.files?.[0])
|
||||
if (autoSubmit) {
|
||||
void formInstance.submit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
/** @file Barrel file for HiddenFile component. */
|
||||
export * from './HiddenFile'
|
@ -7,6 +7,7 @@
|
||||
export * from './ComboBox'
|
||||
export * from './DatePicker'
|
||||
export * from './Dropdown'
|
||||
export * from './HiddenFile'
|
||||
export * from './Input'
|
||||
export * from './MultiSelector'
|
||||
export * from './OTPInput'
|
||||
|
@ -3,12 +3,17 @@
|
||||
*
|
||||
* This module declares the main DOM structure for the authentication/dashboard app.
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { startTransition, StrictMode, useEffect } from 'react'
|
||||
|
||||
import * as sentry from '@sentry/react'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import * as reactDOM from 'react-dom/client'
|
||||
import * as reactRouter from 'react-router-dom'
|
||||
import {
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from 'react-router-dom'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
@ -32,6 +37,9 @@ import { MotionGlobalConfig } from 'framer-motion'
|
||||
|
||||
export type { GraphEditorRunner } from '#/layouts/Editor'
|
||||
|
||||
const HTTP_STATUS_BAD_REQUEST = 400
|
||||
const API_HOST =
|
||||
process.env.ENSO_CLOUD_API_URL != null ? new URL(process.env.ENSO_CLOUD_API_URL).host : null
|
||||
const ARE_ANIMATIONS_DISABLED =
|
||||
window.DISABLE_ANIMATIONS === true ||
|
||||
localStorage.getItem('disableAnimations') === 'true' ||
|
||||
@ -54,23 +62,15 @@ const ROOT_ELEMENT_ID = 'enso-dashboard'
|
||||
/** The fraction of non-erroring interactions that should be sampled by Sentry. */
|
||||
const SENTRY_SAMPLE_RATE = 0.005
|
||||
|
||||
// ======================
|
||||
// === DashboardProps ===
|
||||
// ======================
|
||||
|
||||
/** Props for the dashboard. */
|
||||
export interface DashboardProps extends app.AppProps {
|
||||
readonly logger: Logger
|
||||
}
|
||||
|
||||
// ===========
|
||||
// === run ===
|
||||
// ===========
|
||||
|
||||
/**
|
||||
* Entrypoint for the authentication/dashboard app.
|
||||
*
|
||||
* Running this function finds a `div` element with the ID `dashboard`, and renders the
|
||||
* Running this function finds a `div` element with the ID `enso-dashboard`, and renders the
|
||||
* authentication/dashboard UI using React. It also handles routing and other interactions (e.g.,
|
||||
* for redirecting the user to/from the login page).
|
||||
*/
|
||||
@ -81,18 +81,18 @@ export function run(props: DashboardProps) {
|
||||
process.env.ENSO_CLOUD_SENTRY_DSN != null &&
|
||||
process.env.ENSO_CLOUD_API_URL != null
|
||||
) {
|
||||
const version: unknown = import.meta.env.ENSO_IDE_VERSION
|
||||
sentry.init({
|
||||
dsn: process.env.ENSO_CLOUD_SENTRY_DSN,
|
||||
environment: process.env.ENSO_CLOUD_ENVIRONMENT,
|
||||
release: version?.toString() ?? 'dev',
|
||||
integrations: [
|
||||
new sentry.BrowserTracing({
|
||||
routingInstrumentation: sentry.reactRouterV6Instrumentation(
|
||||
React.useEffect,
|
||||
reactRouter.useLocation,
|
||||
reactRouter.useNavigationType,
|
||||
reactRouter.createRoutesFromChildren,
|
||||
reactRouter.matchRoutes,
|
||||
),
|
||||
sentry.reactRouterV6BrowserTracingIntegration({
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
}),
|
||||
sentry.extraErrorDataIntegration({ captureErrorCause: true }),
|
||||
sentry.replayIntegration(),
|
||||
@ -103,6 +103,22 @@ export function run(props: DashboardProps) {
|
||||
tracePropagationTargets: [process.env.ENSO_CLOUD_API_URL.split('//')[1] ?? ''],
|
||||
replaysSessionSampleRate: SENTRY_SAMPLE_RATE,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
beforeSend: (event) => {
|
||||
if (
|
||||
(event.breadcrumbs ?? []).some(
|
||||
(breadcrumb) =>
|
||||
breadcrumb.type === 'http' &&
|
||||
breadcrumb.category === 'fetch' &&
|
||||
breadcrumb.data &&
|
||||
breadcrumb.data.status_code === HTTP_STATUS_BAD_REQUEST &&
|
||||
typeof breadcrumb.data.url === 'string' &&
|
||||
new URL(breadcrumb.data.url).host === API_HOST,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return event
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -123,9 +139,9 @@ export function run(props: DashboardProps) {
|
||||
|
||||
const httpClient = new HttpClient()
|
||||
|
||||
React.startTransition(() => {
|
||||
startTransition(() => {
|
||||
reactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorBoundary>
|
||||
<UIProviders locale="en-US" portalRoot={portalRoot}>
|
||||
@ -143,7 +159,7 @@ export function run(props: DashboardProps) {
|
||||
</UIProviders>
|
||||
</ErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
</StrictMode>,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export default function InfoMenu(props: InfoMenuProps) {
|
||||
const { getText } = useText()
|
||||
|
||||
return (
|
||||
<Popover {...(!hidden ? { testId: 'info-menu' } : {})} size="xxsmall">
|
||||
<Popover {...(!hidden ? { 'data-testid': 'info-menu' } : {})} size="xxsmall">
|
||||
<div className="mb-2 flex items-center gap-icons overflow-hidden px-menu-entry transition-all duration-user-menu">
|
||||
<SvgMask src={LogoIcon} className="pointer-events-none h-7 w-7 text-primary" />
|
||||
<Text>{PRODUCT_NAME}</Text>
|
||||
|
@ -1,18 +1,16 @@
|
||||
/** @file The input for viewing and changing the organization's profile picture. */
|
||||
import * as React from 'react'
|
||||
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
|
||||
import DefaultUserIcon from '#/assets/default_user.svg'
|
||||
|
||||
import { backendMutationOptions, useBackendQuery } from '#/hooks/backendHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
import FocusRing from '#/components/styled/FocusRing'
|
||||
|
||||
import { Form, HiddenFile } from '#/components/AriaComponents'
|
||||
import type Backend from '#/services/Backend'
|
||||
|
||||
// =======================================
|
||||
@ -29,28 +27,22 @@ export default function OrganizationProfilePictureInput(
|
||||
props: OrganizationProfilePictureInputProps,
|
||||
) {
|
||||
const { backend } = props
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { getText } = textProvider.useText()
|
||||
const { data: organization } = useBackendQuery(backend, 'getOrganization', [])
|
||||
|
||||
const uploadOrganizationPicture = useMutation(
|
||||
backendMutationOptions(backend, 'uploadOrganizationPicture'),
|
||||
).mutate
|
||||
)
|
||||
|
||||
const doUploadOrganizationPicture = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const image = event.target.files?.[0]
|
||||
if (image == null) {
|
||||
toastAndLog('noNewProfilePictureError')
|
||||
} else {
|
||||
uploadOrganizationPicture([{ fileName: image.name }, image])
|
||||
}
|
||||
// Reset selected files, otherwise the file input will do nothing if the same file is
|
||||
// selected again. While technically not undesired behavior, it is unintuitive for the user.
|
||||
event.target.value = ''
|
||||
}
|
||||
const form = Form.useForm({
|
||||
schema: (z) => z.object({ picture: z.instanceof(File) }),
|
||||
onSubmit: async ({ picture }) => {
|
||||
await uploadOrganizationPicture.mutateAsync([{ fileName: picture.name }, picture])
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<FocusRing within>
|
||||
<aria.Label
|
||||
data-testid="organization-profile-picture-input"
|
||||
@ -60,17 +52,12 @@ export default function OrganizationProfilePictureInput(
|
||||
src={organization?.picture ?? DefaultUserIcon}
|
||||
className="pointer-events-none h-full w-full"
|
||||
/>
|
||||
<aria.Input
|
||||
type="file"
|
||||
className="focus-child w-0"
|
||||
accept="image/*"
|
||||
onChange={doUploadOrganizationPicture}
|
||||
/>
|
||||
<HiddenFile autoSubmit form={form} name="picture" />
|
||||
</aria.Label>
|
||||
</FocusRing>
|
||||
<aria.Text className="w-profile-picture-caption py-profile-picture-caption-y">
|
||||
{getText('organizationProfilePictureWarning')}
|
||||
</aria.Text>
|
||||
</>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
@ -6,13 +6,13 @@ import { useMutation } from '@tanstack/react-query'
|
||||
import DefaultUserIcon from '#/assets/default_user.svg'
|
||||
|
||||
import { backendMutationOptions, useBackendQuery } from '#/hooks/backendHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
import FocusRing from '#/components/styled/FocusRing'
|
||||
|
||||
import { Form, HiddenFile } from '#/components/AriaComponents'
|
||||
import type Backend from '#/services/Backend'
|
||||
|
||||
// ===========================
|
||||
@ -27,26 +27,20 @@ export interface ProfilePictureInputProps {
|
||||
/** The input for viewing and changing the user's profile picture. */
|
||||
export default function ProfilePictureInput(props: ProfilePictureInputProps) {
|
||||
const { backend } = props
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { data: user } = useBackendQuery(backend, 'usersMe', [])
|
||||
const { getText } = textProvider.useText()
|
||||
|
||||
const uploadUserPicture = useMutation(backendMutationOptions(backend, 'uploadUserPicture')).mutate
|
||||
const uploadUserPicture = useMutation(backendMutationOptions(backend, 'uploadUserPicture'))
|
||||
|
||||
const doUploadUserPicture = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const image = event.target.files?.[0]
|
||||
if (image == null) {
|
||||
toastAndLog('noNewProfilePictureError')
|
||||
} else {
|
||||
uploadUserPicture([{ fileName: image.name }, image])
|
||||
}
|
||||
// Reset selected files, otherwise the file input will do nothing if the same file is
|
||||
// selected again. While technically not undesired behavior, it is unintuitive for the user.
|
||||
event.target.value = ''
|
||||
}
|
||||
const form = Form.useForm({
|
||||
schema: (z) => z.object({ picture: z.instanceof(File) }),
|
||||
onSubmit: async ({ picture }) => {
|
||||
await uploadUserPicture.mutateAsync([{ fileName: picture.name }, picture])
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<FocusRing within>
|
||||
<aria.Label
|
||||
data-testid="user-profile-picture-input"
|
||||
@ -56,17 +50,12 @@ export default function ProfilePictureInput(props: ProfilePictureInputProps) {
|
||||
src={user?.profilePicture ?? DefaultUserIcon}
|
||||
className="pointer-events-none h-full w-full"
|
||||
/>
|
||||
<aria.Input
|
||||
type="file"
|
||||
className="focus-child w-0"
|
||||
accept="image/*"
|
||||
onChange={doUploadUserPicture}
|
||||
/>
|
||||
<HiddenFile autoSubmit form={form} name="picture" />
|
||||
</aria.Label>
|
||||
</FocusRing>
|
||||
<aria.Text className="w-profile-picture-caption py-profile-picture-caption-y">
|
||||
{getText('profilePictureWarning')}
|
||||
</aria.Text>
|
||||
</>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin'
|
||||
/// <reference types="histoire" />
|
||||
|
||||
import react from '@vitejs/plugin-react'
|
||||
@ -67,6 +68,18 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
...(process.env.NODE_ENV === 'development' ? [await projectManagerShim()] : []),
|
||||
...((
|
||||
process.env.SENTRY_AUTH_TOKEN != null &&
|
||||
process.env.ENSO_CLOUD_SENTRY_ORGANIZATION != null &&
|
||||
process.env.ENSO_CLOUD_SENTRY_PROJECT != null
|
||||
) ?
|
||||
[
|
||||
sentryVitePlugin({
|
||||
org: process.env.ENSO_CLOUD_SENTRY_ORGANIZATION,
|
||||
project: process.env.ENSO_CLOUD_SENTRY_PROJECT,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
optimizeDeps: {
|
||||
entries: fileURLToPath(new URL('./index.html', import.meta.url)),
|
||||
@ -109,6 +122,7 @@ export default defineConfig({
|
||||
build: {
|
||||
// dashboard chunk size is larger than the default warning limit
|
||||
chunkSizeWarningLimit: 700,
|
||||
sourcemap: true,
|
||||
},
|
||||
})
|
||||
async function projectManagerShim(): Promise<Plugin> {
|
||||
|
@ -150,6 +150,10 @@ pub mod secret {
|
||||
// === Github Token ===
|
||||
/// A token created for the `enso-ci` user.
|
||||
pub const CI_PRIVATE_TOKEN: &str = "CI_PRIVATE_TOKEN";
|
||||
|
||||
// === Sentry ===
|
||||
/// The authentication token for pushing source maps to Sentry.
|
||||
pub const SENTRY_AUTH_TOKEN: &str = "SENTRY_AUTH_TOKEN";
|
||||
}
|
||||
|
||||
pub mod variables {
|
||||
@ -446,7 +450,7 @@ impl JobArchetype for UploadIde {
|
||||
"ide upload --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}} --sign-artifacts",
|
||||
)
|
||||
.cleaning(RELEASE_CLEANING_POLICY)
|
||||
.customize(with_packaging_steps(target.0))
|
||||
.customize(with_packaging_steps(target.0, job::PackagingTarget::Release))
|
||||
.build_job("Build IDE", target)
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +154,18 @@ pub fn expose_gui_vars(step: Step) -> Step {
|
||||
expose_cloud_vars(step)
|
||||
}
|
||||
|
||||
/// Expose variables for debugging purposes.
|
||||
pub fn expose_debugging_vars(os: OS, step: Step) -> Step {
|
||||
use crate::ide::web::env::*;
|
||||
match os {
|
||||
OS::Windows => step
|
||||
.with_secret_exposed(secret::SENTRY_AUTH_TOKEN)
|
||||
.with_variable_exposed(ENSO_CLOUD_SENTRY_ORGANIZATION)
|
||||
.with_variable_exposed(ENSO_CLOUD_SENTRY_PROJECT),
|
||||
_ => step,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CancelWorkflow;
|
||||
|
||||
@ -552,13 +564,28 @@ pub fn expose_os_specific_signing_secret(os: OS, step: Step) -> Step {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the artifact is being built for a release build (nightlies, releases)
|
||||
/// or a development build (PRs).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PackagingTarget {
|
||||
/// The artifact is being built for a release build (nightlies, releases).
|
||||
Development,
|
||||
/// The artifact is being built for a development build (PRs).
|
||||
Release,
|
||||
}
|
||||
|
||||
/// Prepares the packaging steps for the given OS.
|
||||
///
|
||||
/// This involves:
|
||||
/// * exposing secrets necessary for code signing and notarization;
|
||||
/// * exposing variables defining cloud environment for dashboard.
|
||||
pub fn prepare_packaging_steps(os: OS, step: Step) -> Vec<Step> {
|
||||
pub fn prepare_packaging_steps(os: OS, step: Step, packaging_target: PackagingTarget) -> Vec<Step> {
|
||||
let step = expose_gui_vars(step);
|
||||
let step = if packaging_target == PackagingTarget::Release {
|
||||
expose_debugging_vars(os, step)
|
||||
} else {
|
||||
step
|
||||
};
|
||||
let step = expose_os_specific_signing_secret(os, step);
|
||||
vec![step]
|
||||
}
|
||||
@ -566,8 +593,11 @@ pub fn prepare_packaging_steps(os: OS, step: Step) -> Vec<Step> {
|
||||
/// Convenience for [`prepare_packaging_steps`].
|
||||
///
|
||||
/// This function is useful when you want to use [`prepare_packaging_steps`] as a closure.
|
||||
pub fn with_packaging_steps(os: OS) -> impl FnOnce(Step) -> Vec<Step> {
|
||||
move |step| prepare_packaging_steps(os, step)
|
||||
pub fn with_packaging_steps(
|
||||
os: OS,
|
||||
packaging_target: PackagingTarget,
|
||||
) -> impl FnOnce(Step) -> Vec<Step> {
|
||||
move |step| prepare_packaging_steps(os, step, packaging_target)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@ -579,7 +609,7 @@ impl JobArchetype for PackageIde {
|
||||
"ide build --backend-source current-ci-run --gui-upload-artifact false",
|
||||
)
|
||||
.customize(move |step| {
|
||||
let mut steps = prepare_packaging_steps(target.0, step);
|
||||
let mut steps = prepare_packaging_steps(target.0, step, PackagingTarget::Development);
|
||||
const TEST_COMMAND: &str = "corepack pnpm -r --filter enso exec playwright test";
|
||||
let test_step = match target.0 {
|
||||
OS::Linux => shell(format!("xvfb-run {TEST_COMMAND}"))
|
||||
|
@ -86,6 +86,12 @@ pub mod env {
|
||||
|
||||
/// The Google Analytics tag to which Google Analytics events should be sent.
|
||||
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG, String;
|
||||
|
||||
/// The Sentry organization to push the source maps to.
|
||||
ENSO_CLOUD_SENTRY_ORGANIZATION, String;
|
||||
|
||||
/// The Sentry project to push the source maps to.
|
||||
ENSO_CLOUD_SENTRY_PROJECT, String;
|
||||
}
|
||||
|
||||
// GUI-specific environment variables
|
||||
|
@ -30,6 +30,9 @@
|
||||
buildInputs = with pkgs; [
|
||||
# === Graal dependencies ===
|
||||
libxcrypt-legacy
|
||||
# === Rust dependencies ===
|
||||
openssl.dev
|
||||
pkg-config
|
||||
] ++ (if !isOnLinux then [
|
||||
# === macOS-specific dependencies ===
|
||||
darwin.apple_sdk.frameworks.IOKit # Required by `enso-formatter`.
|
||||
@ -55,7 +58,7 @@
|
||||
# `sccache` can be used to speed up compile times for Rust crates.
|
||||
# `~/.cargo/bin/sccache` is provided by `cargo install sccache`.
|
||||
# `~/.cargo/bin` must be in the `PATH` for the binary to be accessible.
|
||||
export PATH=$SHIMS_PATH:${rust.out}:$HOME/.cargo/bin:$PATH
|
||||
export PATH=$SHIMS_PATH:$HOME/.cargo/bin:$PATH
|
||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH"
|
||||
|
||||
# `rustup` shim
|
||||
|
573
pnpm-lock.yaml
573
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-08-08"
|
||||
components = ["clippy", "rustfmt"]
|
||||
components = ["clippy", "rustfmt", "rust-src"]
|
||||
profile = "default"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
|
Loading…
Reference in New Issue
Block a user