Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions .github/workflows/ide-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ jobs:
with:
name: fbs-schema
path: engine/language-server/src/main/schema/
- name: Archive project-manager
run: tar -cvf project-manager.tar -C dist/backend .
- name: Upload project-manager
- name: Archive backend
run: tar -cvf backend.tar -C dist/backend .
- name: Upload backend
uses: actions/upload-artifact@v4
with:
name: project-manager-linux
path: project-manager.tar
name: backend-linux
path: backend.tar
- name: Cleanup
run: rm project-manager.tar
run: rm backend.tar
- if: "${{ (always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || (github.ref == 'refs/heads/develop') || inputs.clean_build_required) }}"
name: Clean after
run: corepack pnpm run git-clean --verbose --clean-bazel
Expand Down Expand Up @@ -127,15 +127,15 @@ jobs:
- run: ./run backend get
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Archive project-manager
run: tar -cvf project-manager.tar -C dist/backend .
- name: Upload project-manager
- name: Archive backend
run: tar -cvf backend.tar -C dist/backend .
- name: Upload backend
uses: actions/upload-artifact@v4
with:
name: project-manager-windows
path: project-manager.tar
name: backend-windows
path: backend.tar
- name: Cleanup
run: rm project-manager.tar
run: rm backend.tar
- if: "${{ (always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || (github.ref == 'refs/heads/develop') || inputs.clean_build_required) }}"
name: Clean after
run: corepack pnpm run git-clean --verbose --clean-bazel
Expand Down Expand Up @@ -319,17 +319,16 @@ jobs:
run: corepack pnpm run git-clean --verbose --clean-bazel
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download project-manager
- name: Download backend
uses: actions/download-artifact@v4
with:
name: project-manager-linux
name: backend-linux
path: dist/backend
- run: |-
tar -xvf dist/backend/project-manager.tar -C dist/backend
rm dist/backend/project-manager.tar
tar -xvf dist/backend/backend.tar -C dist/backend
rm dist/backend/backend.tar
- run: ./run ide build --backend-source local --gui-upload-artifact false
env:
ENSO_IDE_HOST: ${{ vars.ENSO_HOST }}
ENSO_IDE_AG_GRID_LICENSE_KEY: ${{ vars.ENSO_AG_GRID_LICENSE_KEY }}
ENSO_IDE_API_URL: ${{ vars.ENSO_CLOUD_API_URL }}
ENSO_IDE_AUTH_ENDPOINT: ${{ vars.ENSO_CLOUD_AUTH_ENDPOINT }}
Expand Down Expand Up @@ -425,14 +424,14 @@ jobs:
run: corepack pnpm run git-clean --verbose --clean-bazel
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download project-manager
- name: Download backend
uses: actions/download-artifact@v4
with:
name: project-manager-windows
name: backend-windows
path: dist/backend
- run: |-
tar -xvf dist/backend/project-manager.tar -C dist/backend
rm dist/backend/project-manager.tar
tar -xvf dist/backend/backend.tar -C dist/backend
rm dist/backend/backend.tar
- run: ./run ide build --backend-source local --gui-upload-artifact false
env:
ENSO_IDE_AG_GRID_LICENSE_KEY: ${{ vars.ENSO_AG_GRID_LICENSE_KEY }}
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run ide upload --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}} --sign-artifacts
env:
ENSO_IDE_HOST: ${{ vars.ENSO_HOST }}
ENSO_IDE_AG_GRID_LICENSE_KEY: ${{ vars.ENSO_AG_GRID_LICENSE_KEY }}
ENSO_IDE_API_URL: ${{ vars.ENSO_CLOUD_API_URL }}
ENSO_IDE_AUTH_ENDPOINT: ${{ vars.ENSO_CLOUD_AUTH_ENDPOINT }}
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

- [Using dual JVM mode for Standard.Microsoft][14476].
- [Standard.Test pending field is lazy][14536].
- [Salesforce OAuth support.][14550]

[14476]: https://github.com/enso-org/enso/pull/14476
[14536]: https://github.com/enso-org/enso/pull/14536
[14550]: https://github.com/enso-org/enso/pull/14550

#### Enso Language & Runtime

Expand Down
2 changes: 1 addition & 1 deletion app/common/.dev-env
1 change: 1 addition & 0 deletions app/common/.env.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ ENSO_IDE_VERSION = "((%__ENSO_IDE_VERSION__%))"
ENSO_IDE_GOOGLE_OAUTH_CLIENT_ID = "((%__ENSO_IDE_GOOGLE_OAUTH_CLIENT_ID__%))"
ENSO_IDE_STRAVA_OAUTH_CLIENT_ID = "((%__ENSO_IDE_STRAVA_OAUTH_CLIENT_ID__%))"
ENSO_IDE_MS365_OAUTH_CLIENT_ID = "((%__ENSO_IDE_MS365_OAUTH_CLIENT_ID__%))"
ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID = "((%__ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID__%))"
3 changes: 3 additions & 0 deletions app/common/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export const $config = {
processEnv.ENSO_IDE_STRAVA_OAUTH_CLIENT_ID ?? import.meta.env?.ENSO_IDE_STRAVA_OAUTH_CLIENT_ID,
MS365_OAUTH_CLIENT_ID:
processEnv.ENSO_IDE_MS365_OAUTH_CLIENT_ID ?? import.meta.env?.ENSO_IDE_MS365_OAUTH_CLIENT_ID,
SALESFORCE_OAUTH_CLIENT_ID:
processEnv.ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID ??
import.meta.env?.ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID,
MAPBOX_API_TOKEN:
(typeof window === 'object' &&
window &&
Expand Down
7 changes: 7 additions & 0 deletions app/common/src/services/Backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,16 @@ export interface MS365CredentialInput {
readonly scopes: readonly string[]
}

/** User settings for a Salesforce credential. */
export interface SalesforceCredentialInput {
readonly type: 'Salesforce'
readonly scopes: readonly string[]
}

/** User settings for an arbitrary credential. */
export type CredentialInput =
| SnowflakeCredentialInput
| GoogleCredentialInput
| StravaCredentialInput
| MS365CredentialInput
| SalesforceCredentialInput
7 changes: 5 additions & 2 deletions app/common/src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,12 @@ export function resolveDictionary() {
*/
export const getText: GetText = (dictionary, key, ...replacements) => {
const template = dictionary[key]
return replacements.length === 0 ?
template
const missingText = `MISSING: ${String(key)}`
return (
template == null ? missingText
: replacements.length === 0 ? template
: template.replace(/[$]([$]|\d+)/g, (_match, placeholder: string) =>
placeholder === '$' ? '$' : String(replacements[Number(placeholder)] ?? `$${placeholder}`),
)
)
}
3 changes: 3 additions & 0 deletions app/common/src/text/english.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@
"ms365CredentialSitesPermissionSitesReadWriteAllDescription": "Allows the app to edit or delete documents and list items in all site collections on behalf of the signed-in user.",
"ms365CredentialSitesPermissionSitesManageAllDescription": "Allows the app to manage and create lists, documents, and list items in all site collections on behalf of the signed-in user.",
"ms365CredentialSitesPermissionNoAccessDescription": "No access to sites.",
"salesforceCredentialType": "Salesforce",
"salesforceCredentialScopesEmptyError": "Please select at least one scope for the integration.",
"salesforceCredentialScopesSummary": "We will request access to manage user data via APIs (api), perform requests at any time (refresh_token, offline_access), and access unique user identifiers (openid).",
"credentialServiceName": "Credential for service",
"credentialState": "State",
"credentialStateReady": "Ready to be used",
Expand Down
3 changes: 3 additions & 0 deletions app/gui/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ setConfig({
processEnv.ENSO_IDE_STRAVA_OAUTH_CLIENT_ID ?? import.meta.env?.ENSO_IDE_STRAVA_OAUTH_CLIENT_ID,
MS365_OAUTH_CLIENT_ID:
processEnv.ENSO_IDE_MS365_OAUTH_CLIENT_ID ?? import.meta.env?.ENSO_IDE_MS365_OAUTH_CLIENT_ID,
SALESFORCE_OAUTH_CLIENT_ID:
processEnv.ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID ??
import.meta.env?.ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID,
MAPBOX_API_TOKEN:
(typeof window === 'object' &&
window &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @file
* Dialog for a Salesforce credential.
* Remember to ensure this component is added to `CREDENTIAL_INFOS` in `constants.ts`.
*/

import { Form } from '#/components/Form'
import { Input } from '#/components/Inputs/Input'
import { Text } from '#/components/Text'
import { useToastAndLog } from '#/hooks/toastAndLogHooks'
import { useText } from '$/providers/react'
import { CredentialsFormFooter } from './CredentialsFormFooter'
import * as salesforce from './salesforce'
import type { CredentialFormProps } from './types'

/** Dialog for a Salesforce credential. */
export function SalesforceCredentialsForm(props: CredentialFormProps) {
const { createCredentials } = props
const { getText } = useText()
const toastAndLog = useToastAndLog()

return (
<Form
method="dialog"
schema={salesforce.FORM_SCHEMA}
defaultValues={{
...salesforce.DEFAULT_FORM_VALUES,
scopes: [...salesforce.DEFAULT_FORM_VALUES.scopes],
}}
className="w-full"
onSubmit={async (values) => {
try {
await salesforce.submitForm(createCredentials, values)
} catch (error) {
toastAndLog(null, error)
}
}}
>
{(form) => (
<>
<Input
form={form}
name="name"
label={getText('name')}
defaultValue={salesforce.DEFAULT_FORM_VALUES.name}
/>
<Text variant="body" color="primary">
{getText('salesforceCredentialScopesSummary')}
</Text>
<CredentialsFormFooter isCreating={true} canCancel={false} canReset={false} />
</>
)}
</Form>
)
}
7 changes: 7 additions & 0 deletions app/gui/src/dashboard/data/serviceCredentials/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @file Constants related to credential dialogs. */
import { GoogleCredentialsForm } from '#/data/serviceCredentials/GoogleCredentialsForm'
import { MS365CredentialsForm } from '#/data/serviceCredentials/MS365CredentialsForm'
import { SalesforceCredentialsForm } from '#/data/serviceCredentials/SalesforceCredentialsForm'
import { SnowflakeCredentialsForm } from '#/data/serviceCredentials/SnowflakeCredentialsForm'
import { StravaCredentialsForm } from '#/data/serviceCredentials/StravaCredentialsForm'
import type { CredentialInfo } from '#/data/serviceCredentials/types'
Expand Down Expand Up @@ -30,4 +31,10 @@ export const CREDENTIAL_INFOS: readonly [CredentialInfo, ...CredentialInfo[]] =
credentialType: 'ms365',
form: MS365CredentialsForm,
},
{
icon: undefined,
nameId: 'salesforceCredentialType',
credentialType: 'salesforce',
form: SalesforceCredentialsForm,
},
]
61 changes: 61 additions & 0 deletions app/gui/src/dashboard/data/serviceCredentials/salesforce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** @file Definitions for the Salesforce credentials integration. */
import type { SalesforceCredentialInput, SecretId } from 'enso-common/src/services/Backend'
import * as i18n from 'enso-common/src/text'
import invariant from 'tiny-invariant'
import { z } from 'zod'
import type { CredentialRecipe } from './types'
import { getOauthRedirectUri } from './utilities'

// Salesforce OAuth scopes required by the integration.
const EXTRA_SCOPES = ['api', 'refresh_token', 'offline_access', 'openid']
const SALESFORCE_OAUTH_AUTHORIZE_URL = 'https://login.salesforce.com/services/oauth2/authorize'

export const FORM_SCHEMA = z.object({
name: z.string().min(1),
scopes: z.array(z.string()).refine((scopes) => scopes.length > 0, {
message: i18n.getText(i18n.resolveDictionary(), 'salesforceCredentialScopesEmptyError'),
}),
})

export const DEFAULT_FORM_VALUES: z.infer<typeof FORM_SCHEMA> = {
name: 'Salesforce',
scopes: EXTRA_SCOPES,
}

/**
* The logic for submitting the Salesforce credential form.
*/
export function submitForm(
createCredentials: (recipe: CredentialRecipe) => Promise<void>,
values: z.infer<typeof FORM_SCHEMA>,
): Promise<void> {
invariant($config.SALESFORCE_OAUTH_CLIENT_ID != null, 'Salesforce OAuth client id is missing')
const salesforceOauthClientId = $config.SALESFORCE_OAUTH_CLIENT_ID

const valuesWithDefaults = { ...DEFAULT_FORM_VALUES, ...values }

const oauthScopes: string[] = [...new Set([...EXTRA_SCOPES, ...valuesWithDefaults.scopes])]
const input: SalesforceCredentialInput = {
type: 'Salesforce',
scopes: oauthScopes,
}
return createCredentials({
name: valuesWithDefaults.name,
input,
makeAuthUrl: (secretId: SecretId, nonce: string) => {
const state = btoa(JSON.stringify({ secretId, nonce }))
const scope = oauthScopes.join(' ')
const query = new URLSearchParams({
/* eslint-disable @typescript-eslint/naming-convention, camelcase */
client_id: salesforceOauthClientId,
redirect_uri: getOauthRedirectUri('Salesforce'),
response_type: 'code',
response_mode: 'query',
state,
scope,
/* eslint-enable @typescript-eslint/naming-convention, camelcase */
})
return `${SALESFORCE_OAUTH_AUTHORIZE_URL}?${query.toString()}`
},
})
}
2 changes: 2 additions & 0 deletions build_tools/build/src/ci_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ pub mod secret {
pub const ENSO_IDE_STRAVA_OAUTH_CLIENT_ID: &str = "ENSO_IDE_STRAVA_OAUTH_CLIENT_ID";
/// The client ID for the MS365 OAuth integration used for MS365 Credentials.
pub const ENSO_IDE_MS365_OAUTH_CLIENT_ID: &str = "ENSO_IDE_MS365_OAUTH_CLIENT_ID";
/// The client ID for the Salesforce OAuth integration used for Salesforce Credentials.
pub const ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID: &str = "ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID";
}

pub mod variables {
Expand Down
4 changes: 4 additions & 0 deletions build_tools/build/src/ci_gen/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ pub fn expose_gui_vars(step: Step) -> Step {
secret::ENSO_IDE_MS365_OAUTH_CLIENT_ID,
ide::web::env::ENSO_IDE_MS365_OAUTH_CLIENT_ID,
)
.with_secret_exposed_as(
secret::ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID,
ide::web::env::ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID,
)
}

/// Expose variables for debugging purposes.
Expand Down
3 changes: 3 additions & 0 deletions build_tools/build/src/ide/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ pub mod env {
/// The client ID for the MS365 OAuth integration used for MS365 Credentials.
ENSO_IDE_MS365_OAUTH_CLIENT_ID, String;

/// The client ID for the Salesforce OAuth integration used for Salesforce Credentials.
ENSO_IDE_SALESFORCE_OAUTH_CLIENT_ID, String;

ENSO_IDE_COMMIT_HASH, String;
ENSO_IDE_VERSION, String;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Enso Signatures 1.0
## module Standard.Saas.Salesforce
- type Salesforce
- initialize credentials:Standard.Base.Any.Any -> Standard.Base.Any.Any
- to_js_object self -> Standard.Base.Any.Any
- user self -> Standard.Base.Any.Any
2 changes: 1 addition & 1 deletion distribution/lib/Standard/Saas/0.0.0-dev/src/Main.enso
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export project.Strava.Strava

export project.Salesforce.Salesforce
41 changes: 41 additions & 0 deletions distribution/lib/Standard/Saas/0.0.0-dev/src/Salesforce.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from Standard.Base import all
from Standard.Base.Enso_Cloud.Enso_Secret import as_credential_reference

polyglot java import org.enso.base.enso_cloud.ExternalLibraryCredentialHelper.CredentialReference
polyglot java import org.enso.saas.salesforce.SalesforceService

type Salesforce
## ---
private: true
---
private Service (salesforce_service:SalesforceService)

## ---
icon: cloud
---
Initializes the Salesforce instance using the given credentials file.

## Arguments:
- `credentials`: a Cloud secret or a file containing Salesforce credentials.
initialize : File | Enso_Secret -> Salesforce
initialize credentials =
credentials_reference = as_credential_reference credentials CredentialReference
Salesforce.Service (SalesforceService.new credentials_reference)

## ---
icon: people
---
Get the Salesforce information about the authenticated user.
user self =
_salesforce_get self "https://login.salesforce.com/services/oauth2/userinfo"

## ---
private: true
---
to_js_object : JS_Object
to_js_object self =
JS_Object.from_pairs [["type", "Salesforce"]]

private _salesforce_get salesforce url =
header = Header.authorization_bearer salesforce.salesforce_service.getAccessToken.token
HTTP.fetch url headers=[header]
Loading
Loading