Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
export class ApStorage {
|
||||
private static instance: Storage;
|
||||
private constructor(value: Storage) {
|
||||
ApStorage.instance = value;
|
||||
}
|
||||
static getInstance() {
|
||||
if (!ApStorage.instance) {
|
||||
ApStorage.instance = window.localStorage;
|
||||
}
|
||||
return ApStorage.instance;
|
||||
}
|
||||
static setInstanceToSessionStorage() {
|
||||
ApStorage.instance = window.sessionStorage;
|
||||
}
|
||||
}
|
||||
171
activepieces-fork/packages/react-ui/src/lib/api.ts
Normal file
171
activepieces-fork/packages/react-ui/src/lib/api.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
HttpStatusCode,
|
||||
isAxiosError,
|
||||
} from 'axios';
|
||||
import qs from 'qs';
|
||||
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
import { ApErrorParams, ErrorCode, isNil } from '@activepieces/shared';
|
||||
|
||||
export const API_BASE_URL =
|
||||
import.meta.env.MODE === 'cloud'
|
||||
? 'https://cloud.activepieces.com'
|
||||
: window.location.origin;
|
||||
export const API_URL = `${API_BASE_URL}/api`;
|
||||
|
||||
const disallowedRoutes = [
|
||||
'/v1/managed-authn/external-token',
|
||||
'/v1/authentication/sign-in',
|
||||
'/v1/authentication/sign-up',
|
||||
'/v1/authn/local/verify-email',
|
||||
'/v1/authn/federated/login',
|
||||
'/v1/authn/federated/claim',
|
||||
'/v1/otp',
|
||||
'/v1/human-input',
|
||||
'/v1/authn/local/reset-password',
|
||||
'/v1/user-invitations/accept',
|
||||
'/v1/webhooks',
|
||||
];
|
||||
//This is important to avoid redirecting to sign-in page when the user is deleted for embedding scenarios
|
||||
const ignroedGlobalErrorHandlerRoutes = ['/v1/users/me'];
|
||||
function isUrlRelative(url: string) {
|
||||
return !url.startsWith('http') && !url.startsWith('https');
|
||||
}
|
||||
|
||||
function globalErrorHandler(error: AxiosError) {
|
||||
if (api.isError(error)) {
|
||||
const errorCode: ErrorCode | undefined = (
|
||||
error.response?.data as { code: ErrorCode }
|
||||
)?.code;
|
||||
if (
|
||||
errorCode === ErrorCode.SESSION_EXPIRED ||
|
||||
errorCode === ErrorCode.INVALID_BEARER_TOKEN
|
||||
) {
|
||||
authenticationSession.logOut();
|
||||
console.log(errorCode);
|
||||
window.location.href = '/sign-in';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function request<TResponse>(
|
||||
url: string,
|
||||
config: AxiosRequestConfig = {},
|
||||
): Promise<TResponse> {
|
||||
const resolvedUrl = !isUrlRelative(url) ? url : `${API_URL}${url}`;
|
||||
const isApWebsite = resolvedUrl.startsWith(API_URL);
|
||||
const unAuthenticated = disallowedRoutes.some((route) =>
|
||||
resolvedUrl.replace(API_URL, '').startsWith(route),
|
||||
);
|
||||
|
||||
return axios({
|
||||
url: resolvedUrl,
|
||||
...config,
|
||||
headers: {
|
||||
...config.headers,
|
||||
Authorization: getToken(
|
||||
unAuthenticated,
|
||||
isApWebsite,
|
||||
authenticationSession.getToken(),
|
||||
),
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
config.responseType === 'blob'
|
||||
? response.data
|
||||
: (response.data as TResponse),
|
||||
)
|
||||
.catch((error) => {
|
||||
if (
|
||||
isAxiosError(error) &&
|
||||
!ignroedGlobalErrorHandlerRoutes.includes(url)
|
||||
) {
|
||||
globalErrorHandler(error);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function getToken(
|
||||
unAuthenticated: boolean,
|
||||
isApWebsite: boolean,
|
||||
token: string | null,
|
||||
) {
|
||||
if (unAuthenticated || !isApWebsite) {
|
||||
return undefined;
|
||||
}
|
||||
if (isNil(token)) {
|
||||
return undefined;
|
||||
}
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
|
||||
export type HttpError = AxiosError<unknown, AxiosResponse<unknown>>;
|
||||
|
||||
export const api = {
|
||||
isApError(error: unknown, errorCode: ErrorCode): error is HttpError {
|
||||
if (!isAxiosError(error)) {
|
||||
return false;
|
||||
}
|
||||
const responseData = error.response?.data as ApErrorParams;
|
||||
return responseData.code === errorCode;
|
||||
},
|
||||
isError(error: unknown): error is HttpError {
|
||||
return isAxiosError(error);
|
||||
},
|
||||
any: <TResponse>(url: string, config?: AxiosRequestConfig) =>
|
||||
request<TResponse>(url, config),
|
||||
get: <TResponse>(url: string, query?: unknown, config?: AxiosRequestConfig) =>
|
||||
request<TResponse>(url, {
|
||||
params: query,
|
||||
paramsSerializer: (params) => {
|
||||
return qs.stringify(params, {
|
||||
arrayFormat: 'repeat',
|
||||
});
|
||||
},
|
||||
...config,
|
||||
}),
|
||||
delete: <TResponse>(
|
||||
url: string,
|
||||
query?: Record<string, string>,
|
||||
body?: unknown,
|
||||
) =>
|
||||
request<TResponse>(url, {
|
||||
method: 'DELETE',
|
||||
params: query,
|
||||
data: body,
|
||||
paramsSerializer: (params) => {
|
||||
return qs.stringify(params, {
|
||||
arrayFormat: 'repeat',
|
||||
});
|
||||
},
|
||||
}),
|
||||
post: <TResponse, TBody = unknown, TParams = unknown>(
|
||||
url: string,
|
||||
body?: TBody,
|
||||
params?: TParams,
|
||||
headers?: Record<string, string>,
|
||||
) =>
|
||||
request<TResponse>(url, {
|
||||
method: 'POST',
|
||||
data: body,
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
params: params,
|
||||
}),
|
||||
|
||||
patch: <TResponse, TBody = unknown, TParams = unknown>(
|
||||
url: string,
|
||||
body?: TBody,
|
||||
params?: TParams,
|
||||
) =>
|
||||
request<TResponse>(url, {
|
||||
method: 'PATCH',
|
||||
data: body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
params: params,
|
||||
}),
|
||||
httpStatus: HttpStatusCode,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
CreateOtpRequestBody,
|
||||
ResetPasswordRequestBody,
|
||||
VerifyEmailRequestBody,
|
||||
} from '@activepieces/ee-shared';
|
||||
import {
|
||||
AuthenticationResponse,
|
||||
ClaimTokenRequest,
|
||||
FederatedAuthnLoginResponse,
|
||||
ProjectRole,
|
||||
SignInRequest,
|
||||
SignUpRequest,
|
||||
SwitchPlatformRequest,
|
||||
SwitchProjectRequest,
|
||||
ThirdPartyAuthnProviderEnum,
|
||||
UserIdentity,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
export const authenticationApi = {
|
||||
signIn(request: SignInRequest) {
|
||||
return api.post<AuthenticationResponse>(
|
||||
'/v1/authentication/sign-in',
|
||||
request,
|
||||
);
|
||||
},
|
||||
signUp(request: SignUpRequest) {
|
||||
return api.post<AuthenticationResponse>(
|
||||
'/v1/authentication/sign-up',
|
||||
request,
|
||||
);
|
||||
},
|
||||
getFederatedAuthLoginUrl(providerName: ThirdPartyAuthnProviderEnum) {
|
||||
return api.get<FederatedAuthnLoginResponse>(`/v1/authn/federated/login`, {
|
||||
providerName,
|
||||
});
|
||||
},
|
||||
getCurrentProjectRole() {
|
||||
return api.get<ProjectRole | null>('/v1/project-members/role');
|
||||
},
|
||||
claimThirdPartyRequest(request: ClaimTokenRequest) {
|
||||
return api.post<AuthenticationResponse>(
|
||||
'/v1/authn/federated/claim',
|
||||
request,
|
||||
);
|
||||
},
|
||||
sendOtpEmail(request: CreateOtpRequestBody) {
|
||||
return api.post<void>('/v1/otp', request);
|
||||
},
|
||||
resetPassword(request: ResetPasswordRequestBody) {
|
||||
return api.post<void>('/v1/authn/local/reset-password', request);
|
||||
},
|
||||
verifyEmail(request: VerifyEmailRequestBody) {
|
||||
return api.post<UserIdentity>('/v1/authn/local/verify-email', request);
|
||||
},
|
||||
switchProject(request: SwitchProjectRequest) {
|
||||
return api.post<AuthenticationResponse>(
|
||||
`/v1/authentication/switch-project`,
|
||||
request,
|
||||
);
|
||||
},
|
||||
switchPlatform(request: SwitchPlatformRequest) {
|
||||
return api.post<AuthenticationResponse>(
|
||||
`/v1/authentication/switch-platform`,
|
||||
request,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
import {
|
||||
AuthenticationResponse,
|
||||
isNil,
|
||||
UserPrincipal,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import { ApStorage } from './ap-browser-storage';
|
||||
import { authenticationApi } from './authentication-api';
|
||||
const tokenKey = 'token';
|
||||
|
||||
export const authenticationSession = {
|
||||
saveResponse(response: AuthenticationResponse, isEmbedding: boolean) {
|
||||
if (isEmbedding) {
|
||||
ApStorage.setInstanceToSessionStorage();
|
||||
}
|
||||
ApStorage.getInstance().setItem(tokenKey, response.token);
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
},
|
||||
isJwtExpired(token: string): boolean {
|
||||
if (!token) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const decoded = jwtDecode(token);
|
||||
if (decoded && decoded.exp && dayjs().isAfter(dayjs.unix(decoded.exp))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
getToken(): string | null {
|
||||
return ApStorage.getInstance().getItem(tokenKey) ?? null;
|
||||
},
|
||||
|
||||
getProjectId(): string | null {
|
||||
const token = this.getToken();
|
||||
if (isNil(token)) {
|
||||
return null;
|
||||
}
|
||||
const decodedJwt = getDecodedJwt(token);
|
||||
return decodedJwt.projectId;
|
||||
},
|
||||
getCurrentUserId(): string | null {
|
||||
const token = this.getToken();
|
||||
if (isNil(token)) {
|
||||
return null;
|
||||
}
|
||||
const decodedJwt = getDecodedJwt(token);
|
||||
return decodedJwt.id;
|
||||
},
|
||||
appendProjectRoutePrefix(path: string): string {
|
||||
const projectId = this.getProjectId();
|
||||
if (isNil(projectId)) {
|
||||
return path;
|
||||
}
|
||||
return `/projects/${projectId}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
},
|
||||
getPlatformId(): string | null {
|
||||
const token = this.getToken();
|
||||
if (isNil(token)) {
|
||||
return null;
|
||||
}
|
||||
const decodedJwt = getDecodedJwt(token);
|
||||
return decodedJwt.platform.id;
|
||||
},
|
||||
async switchToPlatform(platformId: string) {
|
||||
if (authenticationSession.getPlatformId() === platformId) {
|
||||
return;
|
||||
}
|
||||
const result = await authenticationApi.switchPlatform({
|
||||
platformId,
|
||||
});
|
||||
ApStorage.getInstance().setItem(tokenKey, result.token);
|
||||
window.location.href = '/';
|
||||
},
|
||||
async switchToProject(projectId: string) {
|
||||
if (authenticationSession.getProjectId() === projectId) {
|
||||
return;
|
||||
}
|
||||
const result = await authenticationApi.switchProject({ projectId });
|
||||
ApStorage.getInstance().setItem(tokenKey, result.token);
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
},
|
||||
isLoggedIn(): boolean {
|
||||
const token = this.getToken();
|
||||
if (isNil(token)) {
|
||||
return false;
|
||||
}
|
||||
return !this.isJwtExpired(token);
|
||||
},
|
||||
clearSession() {
|
||||
ApStorage.getInstance().removeItem(tokenKey);
|
||||
},
|
||||
logOut() {
|
||||
this.clearSession();
|
||||
window.location.href = '/sign-in';
|
||||
},
|
||||
};
|
||||
|
||||
function getDecodedJwt(token: string): UserPrincipal {
|
||||
return jwtDecode<UserPrincipal>(token);
|
||||
}
|
||||
71
activepieces-fork/packages/react-ui/src/lib/color-util.ts
Normal file
71
activepieces-fork/packages/react-ui/src/lib/color-util.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
const hexToHslString = (hex: string) => {
|
||||
const { hue, saturation, lightness } = parseToHsl(hex);
|
||||
return `${hue.toFixed(1)} ${(saturation * 100).toFixed(1)}% ${(
|
||||
lightness * 100
|
||||
).toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const parseToHsl = (hex: string) => {
|
||||
// Remove the '#' character if it exists
|
||||
hex = hex.replace(/^#/, '');
|
||||
|
||||
// Convert 3-digit hex to 6-digit hex
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Convert hex to RGB
|
||||
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
||||
|
||||
// Find the maximum and minimum values to get lightness
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
|
||||
// Calculate lightness
|
||||
const lightness = (max + min) / 2;
|
||||
|
||||
let hue = 0;
|
||||
let saturation = 0;
|
||||
|
||||
if (max !== min) {
|
||||
const delta = max - min;
|
||||
|
||||
// Calculate saturation
|
||||
saturation =
|
||||
lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
||||
|
||||
// Calculate hue
|
||||
switch (max) {
|
||||
case r:
|
||||
hue = (g - b) / delta + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
hue = (b - r) / delta + 2;
|
||||
break;
|
||||
case b:
|
||||
hue = (r - g) / delta + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
hue /= 6;
|
||||
}
|
||||
|
||||
// Convert hue to degrees
|
||||
hue = hue * 360;
|
||||
|
||||
return {
|
||||
hue,
|
||||
saturation,
|
||||
lightness,
|
||||
};
|
||||
};
|
||||
|
||||
export const colorsUtils = {
|
||||
hexToHslString,
|
||||
parseToHsl,
|
||||
};
|
||||
36
activepieces-fork/packages/react-ui/src/lib/compose-refs.ts
Normal file
36
activepieces-fork/packages/react-ui/src/lib/compose-refs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
type PossibleRef<T> = React.Ref<T> | undefined;
|
||||
|
||||
/**
|
||||
* Set a given ref to a given value
|
||||
* This utility takes care of different types of refs: callback refs and RefObject(s)
|
||||
*/
|
||||
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(value);
|
||||
} else if (ref !== null && ref !== undefined) {
|
||||
(ref as React.MutableRefObject<T>).current = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility to compose multiple refs together
|
||||
* Accepts callback refs and RefObject(s)
|
||||
*/
|
||||
function composeRefs<T>(...refs: PossibleRef<T>[]) {
|
||||
return (node: T) => refs.forEach((ref) => setRef(ref, node));
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom hook that composes multiple refs
|
||||
* Accepts callback refs and RefObject(s)
|
||||
*/
|
||||
function useComposedRefs<T>(...refs: PossibleRef<T>[]) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return React.useCallback(composeRefs(...refs), refs);
|
||||
}
|
||||
|
||||
export { composeRefs, useComposedRefs };
|
||||
8
activepieces-fork/packages/react-ui/src/lib/flags-api.ts
Normal file
8
activepieces-fork/packages/react-ui/src/lib/flags-api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { api } from './api';
|
||||
|
||||
export type FlagsMap = Record<string, boolean | string | object | undefined>;
|
||||
export const flagsApi = {
|
||||
getAll() {
|
||||
return api.get<FlagsMap>(`/v1/flags`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GetSystemHealthChecksResponse } from '@activepieces/shared';
|
||||
|
||||
import { api } from './api';
|
||||
|
||||
export const healthApi = {
|
||||
getSystemHealthChecks(): Promise<GetSystemHealthChecksResponse> {
|
||||
return api.get<GetSystemHealthChecksResponse>('/v1/health/system');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { api } from '@/lib/api';
|
||||
import { AuthenticationResponse } from '@activepieces/shared';
|
||||
|
||||
/**
|
||||
* Django Trust Authentication API
|
||||
*
|
||||
* Uses the custom django-trust endpoint instead of EE managed-authn.
|
||||
* This allows SmoothSchedule to embed Activepieces using Django-signed JWTs.
|
||||
*/
|
||||
export const managedAuthApi = {
|
||||
generateApToken: async (request: { externalAccessToken: string }) => {
|
||||
// Use our custom django-trust authentication endpoint
|
||||
return api.post<AuthenticationResponse>(
|
||||
`/v1/authentication/django-trust`,
|
||||
{ token: request.externalAccessToken },
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useEmbedding } from '../components/embed-provider';
|
||||
|
||||
export const useNewWindow = () => {
|
||||
const { embedState } = useEmbedding();
|
||||
const navigate = useNavigate();
|
||||
if (embedState.isEmbedded) {
|
||||
return (route: string, searchParams?: string) =>
|
||||
navigate({
|
||||
pathname: route,
|
||||
search: searchParams,
|
||||
});
|
||||
} else {
|
||||
return (route: string, searchParams?: string) =>
|
||||
window.open(
|
||||
`${route}${searchParams ? '?' + searchParams : ''}`,
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const FROM_QUERY_PARAM = 'from';
|
||||
/**State param is for oauth2 flow, it is used to redirect to the page after login*/
|
||||
export const STATE_QUERY_PARAM = 'state';
|
||||
export const LOGIN_QUERY_PARAM = 'activepiecesLogin';
|
||||
export const PROVIDER_NAME_QUERY_PARAM = 'providerName';
|
||||
|
||||
export const useDefaultRedirectPath = () => {
|
||||
return '/flows';
|
||||
};
|
||||
|
||||
export const useRedirectAfterLogin = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const defaultRedirectPath = useDefaultRedirectPath();
|
||||
const from = searchParams.get(FROM_QUERY_PARAM) ?? defaultRedirectPath;
|
||||
return () => navigate(from);
|
||||
};
|
||||
208
activepieces-fork/packages/react-ui/src/lib/oauth2-utils.ts
Normal file
208
activepieces-fork/packages/react-ui/src/lib/oauth2-utils.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { OAuth2Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
AppConnectionType,
|
||||
BOTH_CLIENT_CREDENTIALS_AND_AUTHORIZATION_CODE,
|
||||
isNil,
|
||||
OAuth2GrantType,
|
||||
ThirdPartyAuthnProviderEnum,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import {
|
||||
FROM_QUERY_PARAM,
|
||||
LOGIN_QUERY_PARAM,
|
||||
PROVIDER_NAME_QUERY_PARAM,
|
||||
STATE_QUERY_PARAM,
|
||||
} from './navigation-utils';
|
||||
|
||||
let currentPopup: Window | null = null;
|
||||
|
||||
function useThirdPartyLogin() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (loginUrl: string, providerName: ThirdPartyAuthnProviderEnum) => {
|
||||
const from = searchParams.get(FROM_QUERY_PARAM) || '/flows';
|
||||
const state = {
|
||||
[PROVIDER_NAME_QUERY_PARAM]: providerName,
|
||||
[FROM_QUERY_PARAM]: from,
|
||||
[LOGIN_QUERY_PARAM]: 'true',
|
||||
};
|
||||
const loginUrlWithState = new URL(loginUrl);
|
||||
loginUrlWithState.searchParams.set(
|
||||
STATE_QUERY_PARAM,
|
||||
JSON.stringify(state),
|
||||
);
|
||||
window.location.href = loginUrlWithState.toString();
|
||||
};
|
||||
}
|
||||
|
||||
async function openOAuth2Popup(
|
||||
params: OAuth2PopupParams,
|
||||
): Promise<OAuth2PopupResponse> {
|
||||
closeOAuth2Popup();
|
||||
const pckeChallenge = nanoid(43);
|
||||
const url = await constructUrl(params, pckeChallenge);
|
||||
currentPopup = openWindow(url);
|
||||
return {
|
||||
code: await getCode(params.redirectUrl),
|
||||
codeChallenge: params.pkce ? pckeChallenge : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function openWindow(url: string): Window | null {
|
||||
const winFeatures = [
|
||||
'resizable=no',
|
||||
'toolbar=no',
|
||||
'left=100',
|
||||
'top=100',
|
||||
'scrollbars=no',
|
||||
'menubar=no',
|
||||
'status=no',
|
||||
'directories=no',
|
||||
'location=no',
|
||||
'width=600',
|
||||
'height=800',
|
||||
].join(', ');
|
||||
return window.open(url, '_blank', winFeatures);
|
||||
}
|
||||
|
||||
function closeOAuth2Popup() {
|
||||
currentPopup?.close();
|
||||
}
|
||||
|
||||
async function generateCodeChallenge(codeVerifier: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(codeVerifier);
|
||||
const digest = await window.crypto.subtle.digest('SHA-256', data);
|
||||
|
||||
const base64String = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
||||
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
async function constructUrl(params: OAuth2PopupParams, pckeChallenge: string) {
|
||||
const queryParams: Record<string, string> = {
|
||||
response_type: 'code',
|
||||
client_id: params.clientId,
|
||||
redirect_uri: params.redirectUrl,
|
||||
access_type: 'offline',
|
||||
state: nanoid(),
|
||||
prompt: 'consent',
|
||||
scope: params.scope,
|
||||
...(params.extraParams || {}),
|
||||
};
|
||||
|
||||
if (params.prompt === 'omit') {
|
||||
delete queryParams['prompt'];
|
||||
} else if (!isNil(params.prompt)) {
|
||||
queryParams['prompt'] = params.prompt;
|
||||
}
|
||||
|
||||
if (params.pkce) {
|
||||
const method = params.pkceMethod || 'plain';
|
||||
queryParams['code_challenge_method'] = method;
|
||||
|
||||
if (method === 'S256') {
|
||||
queryParams['code_challenge'] = await generateCodeChallenge(
|
||||
pckeChallenge,
|
||||
);
|
||||
} else {
|
||||
queryParams['code_challenge'] = pckeChallenge;
|
||||
}
|
||||
}
|
||||
const url = new URL(params.authUrl);
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
if (value !== '') {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getCode(redirectUrl: string): Promise<string> {
|
||||
return new Promise<string>((resolve) => {
|
||||
window.addEventListener('message', function handler(event) {
|
||||
if (
|
||||
redirectUrl &&
|
||||
redirectUrl.startsWith(event.origin) &&
|
||||
event.data['code']
|
||||
) {
|
||||
resolve(decodeURIComponent(event.data.code));
|
||||
closeOAuth2Popup();
|
||||
window.removeEventListener('message', handler);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type OAuth2PopupParams = {
|
||||
authUrl: string;
|
||||
clientId: string;
|
||||
redirectUrl: string;
|
||||
scope: string;
|
||||
prompt?: 'none' | 'consent' | 'login' | 'omit';
|
||||
pkce: boolean;
|
||||
pkceMethod?: 'plain' | 'S256';
|
||||
extraParams?: Record<string, string>;
|
||||
};
|
||||
|
||||
type OAuth2PopupResponse = {
|
||||
code: string;
|
||||
codeChallenge: string | undefined;
|
||||
};
|
||||
|
||||
function getGrantType(property: OAuth2Property<any>) {
|
||||
if (
|
||||
isNil(property.grantType) ||
|
||||
property.grantType === BOTH_CLIENT_CREDENTIALS_AND_AUTHORIZATION_CODE
|
||||
) {
|
||||
return OAuth2GrantType.AUTHORIZATION_CODE;
|
||||
}
|
||||
return property.grantType;
|
||||
}
|
||||
|
||||
function getPredefinedOAuth2App(
|
||||
piecesOAuth2AppsMap: PiecesOAuth2AppsMap,
|
||||
pieceName: string,
|
||||
): OAuth2App | null {
|
||||
const pieceOAuth2Apps = piecesOAuth2AppsMap[pieceName];
|
||||
if (isNil(pieceOAuth2Apps)) {
|
||||
return null;
|
||||
}
|
||||
if (pieceOAuth2Apps.platformOAuth2App) {
|
||||
return pieceOAuth2Apps.platformOAuth2App;
|
||||
}
|
||||
if (pieceOAuth2Apps.cloudOAuth2App) {
|
||||
return pieceOAuth2Apps.cloudOAuth2App;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type OAuth2App =
|
||||
| {
|
||||
oauth2Type:
|
||||
| AppConnectionType.CLOUD_OAUTH2
|
||||
| AppConnectionType.PLATFORM_OAUTH2;
|
||||
clientId: string;
|
||||
}
|
||||
| {
|
||||
oauth2Type: AppConnectionType.OAUTH2;
|
||||
clientId: null;
|
||||
};
|
||||
|
||||
export type PiecesOAuth2AppsMap = Record<
|
||||
string,
|
||||
| {
|
||||
cloudOAuth2App: OAuth2App | null;
|
||||
platformOAuth2App: OAuth2App | null;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
|
||||
export const oauth2Utils = {
|
||||
openOAuth2Popup,
|
||||
useThirdPartyLogin,
|
||||
getGrantType,
|
||||
getPredefinedOAuth2App,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { api } from '@/lib/api';
|
||||
import { ListProjectRequestForPlatformQueryParams } from '@activepieces/ee-shared';
|
||||
import { ProjectWithLimits, SeekPage } from '@activepieces/shared';
|
||||
|
||||
export const platformProjectApi = {
|
||||
list: async (
|
||||
params: ListProjectRequestForPlatformQueryParams,
|
||||
): Promise<SeekPage<ProjectWithLimits>> => {
|
||||
return api.get<SeekPage<ProjectWithLimits>>(`/v1/projects`, params);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
SeekPage,
|
||||
UpdateUserRequestBody,
|
||||
User,
|
||||
UserWithMetaInformation,
|
||||
ListUsersRequestBody,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
export const platformUserApi = {
|
||||
list(request: ListUsersRequestBody) {
|
||||
return api.get<SeekPage<UserWithMetaInformation>>('/v1/users', request);
|
||||
},
|
||||
delete(id: string) {
|
||||
return api.delete(`/v1/users/${id}`);
|
||||
},
|
||||
update(id: string, request: UpdateUserRequestBody): Promise<User> {
|
||||
return api.post<User>(`/v1/users/${id}`, request);
|
||||
},
|
||||
};
|
||||
50
activepieces-fork/packages/react-ui/src/lib/platforms-api.ts
Normal file
50
activepieces-fork/packages/react-ui/src/lib/platforms-api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
PlatformWithoutSensitiveData,
|
||||
UpdatePlatformRequestBody,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import { api } from './api';
|
||||
import { authenticationSession } from './authentication-session';
|
||||
|
||||
export const platformApi = {
|
||||
deleteAccount() {
|
||||
return api.delete<void>(
|
||||
`/v1/platforms/${authenticationSession.getPlatformId()}`,
|
||||
);
|
||||
},
|
||||
getCurrentPlatform() {
|
||||
const platformId = authenticationSession.getPlatformId();
|
||||
if (!platformId) {
|
||||
throw Error('No platform id found');
|
||||
}
|
||||
return api.get<PlatformWithoutSensitiveData>(`/v1/platforms/${platformId}`);
|
||||
},
|
||||
|
||||
verifyLicenseKey(licenseKey: string) {
|
||||
const platformId = authenticationSession.getPlatformId();
|
||||
if (!platformId) {
|
||||
throw Error('No platform id found');
|
||||
}
|
||||
return api.post<void>(`/v1/license-keys/verify`, {
|
||||
platformId,
|
||||
licenseKey,
|
||||
});
|
||||
},
|
||||
|
||||
update(req: UpdatePlatformRequestBody, platformId: string) {
|
||||
return api.post<PlatformWithoutSensitiveData>(
|
||||
`/v1/platforms/${platformId}`,
|
||||
req,
|
||||
);
|
||||
},
|
||||
updateWithFormData(formdata: FormData, platformId: string) {
|
||||
return api.post<PlatformWithoutSensitiveData>(
|
||||
`/v1/platforms/${platformId}`,
|
||||
formdata,
|
||||
{},
|
||||
{
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
38
activepieces-fork/packages/react-ui/src/lib/project-api.ts
Normal file
38
activepieces-fork/packages/react-ui/src/lib/project-api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { api } from '@/lib/api';
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
import {
|
||||
CreatePlatformProjectRequest,
|
||||
UpdateProjectPlatformRequest,
|
||||
} from '@activepieces/ee-shared';
|
||||
import {
|
||||
ListProjectRequestForUserQueryParams,
|
||||
ProjectWithLimits,
|
||||
ProjectWithLimitsWithPlatform,
|
||||
SeekPage,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
export const projectApi = {
|
||||
current: async () => {
|
||||
return projectApi.get(authenticationSession.getProjectId()!);
|
||||
},
|
||||
list(request: ListProjectRequestForUserQueryParams) {
|
||||
return api.get<SeekPage<ProjectWithLimits>>('/v1/users/projects', request);
|
||||
},
|
||||
get: async (projectId: string) => {
|
||||
return api.get<ProjectWithLimits>(`/v1/users/projects/${projectId}`);
|
||||
},
|
||||
update: async (projectId: string, request: UpdateProjectPlatformRequest) => {
|
||||
return api.post<ProjectWithLimits>(`/v1/projects/${projectId}`, request);
|
||||
},
|
||||
create: async (request: CreatePlatformProjectRequest) => {
|
||||
return api.post<ProjectWithLimits>('/v1/projects', request);
|
||||
},
|
||||
delete: async (projectId: string) => {
|
||||
return api.delete<void>(`/v1/projects/${projectId}`);
|
||||
},
|
||||
listForPlatforms: async () => {
|
||||
return api.get<ProjectWithLimitsWithPlatform[]>(
|
||||
'/v1/users/projects/platforms',
|
||||
);
|
||||
},
|
||||
};
|
||||
29
activepieces-fork/packages/react-ui/src/lib/promise-queue.ts
Normal file
29
activepieces-fork/packages/react-ui/src/lib/promise-queue.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
export class PromiseQueue {
|
||||
private queue: (() => Promise<unknown>)[] = [];
|
||||
private lock: Mutex = new Mutex();
|
||||
private halted = false;
|
||||
|
||||
add(promise: () => Promise<unknown>) {
|
||||
this.queue.push(promise);
|
||||
this.run();
|
||||
}
|
||||
|
||||
halt() {
|
||||
this.halted = true;
|
||||
}
|
||||
size() {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
private run() {
|
||||
this.lock.runExclusive(async () => {
|
||||
const promise = this.queue.shift()!;
|
||||
if (this.halted) {
|
||||
return;
|
||||
}
|
||||
await promise();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CreateTrialLicenseKeyRequestBody } from '@activepieces/shared';
|
||||
|
||||
import { api } from './api';
|
||||
import { flagsApi } from './flags-api';
|
||||
|
||||
export const requestTrialApi = {
|
||||
createKey(params: CreateTrialLicenseKeyRequestBody): Promise<void> {
|
||||
return api.post<void>(`/v1/license-keys`, params);
|
||||
},
|
||||
async contactSales(params: ContactSalesRequest): Promise<void> {
|
||||
const flags = await flagsApi.getAll();
|
||||
return api.post<void>(
|
||||
`https://sales.activepieces.com/submit-inapp-contact-form`,
|
||||
{
|
||||
...params,
|
||||
flags: flags,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
type ContactSalesRequest = {
|
||||
fullName: string;
|
||||
email: string;
|
||||
companyName: string;
|
||||
goal: string;
|
||||
numberOfEmployees: string;
|
||||
};
|
||||
107
activepieces-fork/packages/react-ui/src/lib/types.ts
Normal file
107
activepieces-fork/packages/react-ui/src/lib/types.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
ActionBase,
|
||||
ErrorHandlingOptionsParam,
|
||||
PieceAuthProperty,
|
||||
PieceMetadataModelSummary,
|
||||
TriggerBase,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
FlowActionType,
|
||||
PackageType,
|
||||
PieceType,
|
||||
FlowTriggerType,
|
||||
FlowOperationType,
|
||||
StepLocationRelativeToParent,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
type BaseStepMetadata = {
|
||||
displayName: string;
|
||||
logoUrl: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type PieceStepMetadata = BaseStepMetadata & {
|
||||
type: FlowActionType.PIECE | FlowTriggerType.PIECE;
|
||||
pieceName: string;
|
||||
pieceVersion: string;
|
||||
categories: string[];
|
||||
packageType: PackageType;
|
||||
pieceType: PieceType;
|
||||
auth: PieceAuthProperty | PieceAuthProperty[] | undefined;
|
||||
errorHandlingOptions?: ErrorHandlingOptionsParam;
|
||||
};
|
||||
|
||||
export type PrimitiveStepMetadata = BaseStepMetadata & {
|
||||
type:
|
||||
| FlowActionType.CODE
|
||||
| FlowActionType.LOOP_ON_ITEMS
|
||||
| FlowActionType.ROUTER
|
||||
| FlowTriggerType.EMPTY;
|
||||
};
|
||||
|
||||
export type PieceStepMetadataWithSuggestions = PieceStepMetadata &
|
||||
Pick<PieceMetadataModelSummary, 'suggestedActions' | 'suggestedTriggers'>;
|
||||
|
||||
export type StepMetadataWithSuggestions =
|
||||
| PieceStepMetadataWithSuggestions
|
||||
| PrimitiveStepMetadata;
|
||||
|
||||
export type CategorizedStepMetadataWithSuggestions = {
|
||||
title: string;
|
||||
metadata: StepMetadataWithSuggestions[];
|
||||
};
|
||||
|
||||
export type StepMetadata = PieceStepMetadata | PrimitiveStepMetadata;
|
||||
|
||||
export type StepMetadataWithActionOrTriggerOrAgentDisplayName = StepMetadata & {
|
||||
actionOrTriggerOrAgentDisplayName: string;
|
||||
actionOrTriggerOrAgentDescription: string;
|
||||
};
|
||||
|
||||
export type PieceSelectorOperation =
|
||||
| {
|
||||
type: FlowOperationType.ADD_ACTION;
|
||||
actionLocation: {
|
||||
branchIndex: number;
|
||||
parentStep: string;
|
||||
stepLocationRelativeToParent: StepLocationRelativeToParent.INSIDE_BRANCH;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: FlowOperationType.ADD_ACTION;
|
||||
actionLocation: {
|
||||
parentStep: string;
|
||||
stepLocationRelativeToParent: Exclude<
|
||||
StepLocationRelativeToParent,
|
||||
StepLocationRelativeToParent.INSIDE_BRANCH
|
||||
>;
|
||||
};
|
||||
}
|
||||
| { type: FlowOperationType.UPDATE_TRIGGER }
|
||||
| {
|
||||
type: FlowOperationType.UPDATE_ACTION;
|
||||
stepName: string;
|
||||
};
|
||||
|
||||
export type AskAiButtonOperations = Exclude<
|
||||
PieceSelectorOperation,
|
||||
{ type: FlowOperationType.UPDATE_TRIGGER }
|
||||
>;
|
||||
|
||||
export type PieceSelectorPieceItem =
|
||||
| {
|
||||
actionOrTrigger: TriggerBase;
|
||||
type: FlowTriggerType.PIECE;
|
||||
pieceMetadata: PieceStepMetadata;
|
||||
}
|
||||
| ({
|
||||
actionOrTrigger: ActionBase;
|
||||
type: FlowActionType.PIECE;
|
||||
pieceMetadata: PieceStepMetadata;
|
||||
} & {
|
||||
auth?: PieceAuthProperty;
|
||||
});
|
||||
|
||||
export type PieceSelectorItem = PieceSelectorPieceItem | PrimitiveStepMetadata;
|
||||
|
||||
export type HandleSelectActionOrTrigger = (item: PieceSelectorItem) => void;
|
||||
9
activepieces-fork/packages/react-ui/src/lib/user-api.ts
Normal file
9
activepieces-fork/packages/react-ui/src/lib/user-api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { UserWithMetaInformationAndProject } from '@activepieces/shared';
|
||||
|
||||
import { api } from './api';
|
||||
|
||||
export const userApi = {
|
||||
getCurrentUser() {
|
||||
return api.get<UserWithMetaInformationAndProject>('/v1/users/me');
|
||||
},
|
||||
};
|
||||
404
activepieces-fork/packages/react-ui/src/lib/utils.ts
Normal file
404
activepieces-fork/packages/react-ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import i18next, { t } from 'i18next';
|
||||
import JSZip from 'jszip';
|
||||
import { useEffect, useRef, useState, RefObject } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { LocalesEnum, Permission } from '@activepieces/shared';
|
||||
|
||||
import { authenticationSession } from './authentication-session';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
const emailRegex =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export const formatUtils = {
|
||||
emailRegex,
|
||||
convertEnumToHumanReadable(str: string) {
|
||||
const words = str.split(/[_.]/);
|
||||
return words
|
||||
.map(
|
||||
(word) =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1).toLocaleLowerCase(),
|
||||
)
|
||||
.join(' ');
|
||||
},
|
||||
convertEnumToReadable(value: string): string {
|
||||
return (
|
||||
value.charAt(0).toUpperCase() +
|
||||
value.slice(1).toLowerCase().replace(/_/g, ' ')
|
||||
);
|
||||
},
|
||||
formatNumber(number: number) {
|
||||
return new Intl.NumberFormat(i18next.language).format(number);
|
||||
},
|
||||
formatDateOnlyOrFail(date: Date, fallback: string) {
|
||||
try {
|
||||
return this.formatDateOnly(date);
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
formatDateOnly(date: Date) {
|
||||
return Intl.DateTimeFormat(i18next.language, {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
},
|
||||
formatDateWithTime(date: Date, hideCurrentYear: boolean) {
|
||||
const now = dayjs();
|
||||
const inputDate = dayjs(date);
|
||||
const isToday = inputDate.isSame(now, 'day');
|
||||
const isYesterday = inputDate.isSame(now.subtract(1, 'day'), 'day');
|
||||
const isSameYear = inputDate.isSame(now, 'year');
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat(i18next.language, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
if (isToday) {
|
||||
return `${t('Today')}, ${timeFormat.format(date)}`;
|
||||
} else if (isYesterday) {
|
||||
return `${t('Yesterday')}, ${timeFormat.format(date)}`;
|
||||
}
|
||||
|
||||
if (isSameYear && !hideCurrentYear) {
|
||||
return Intl.DateTimeFormat(i18next.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
return Intl.DateTimeFormat(i18next.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
}).format(date);
|
||||
},
|
||||
formatDate(date: Date) {
|
||||
const now = dayjs();
|
||||
const inputDate = dayjs(date);
|
||||
const isToday = inputDate.isSame(now, 'day');
|
||||
const isYesterday = inputDate.isSame(now.subtract(1, 'day'), 'day');
|
||||
const isSameYear = inputDate.isSame(now, 'year');
|
||||
|
||||
if (isToday) {
|
||||
return t('Today');
|
||||
}
|
||||
|
||||
if (isYesterday) {
|
||||
return t('Yesterday');
|
||||
}
|
||||
|
||||
if (isSameYear) {
|
||||
return Intl.DateTimeFormat(i18next.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
return Intl.DateTimeFormat(i18next.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
},
|
||||
formatToHoursAndMinutes(minutes: number) {
|
||||
if (minutes < 60) {
|
||||
return `${formatUtils.formatNumber(minutes)} mins`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${formatUtils.formatNumber(hours)} hours`;
|
||||
},
|
||||
formatDateToAgo(date: Date) {
|
||||
const now = dayjs();
|
||||
const inputDate = dayjs(date);
|
||||
const diffInSeconds = now.diff(inputDate, 'second');
|
||||
const diffInMinutes = now.diff(inputDate, 'minute');
|
||||
const diffInHours = now.diff(inputDate, 'hour');
|
||||
const diffInDays = now.diff(inputDate, 'day');
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return `${diffInSeconds}s ago`;
|
||||
}
|
||||
if (diffInMinutes < 60) {
|
||||
return `${diffInMinutes}m ago`;
|
||||
}
|
||||
if (diffInHours < 24) {
|
||||
return `${diffInHours}h ago`;
|
||||
}
|
||||
if (diffInDays < 30) {
|
||||
return `${diffInDays}d ago`;
|
||||
}
|
||||
return inputDate.format('MMM D, YYYY');
|
||||
},
|
||||
formatDuration(durationMs: number | undefined, short?: boolean): string {
|
||||
if (durationMs === undefined) {
|
||||
return '-';
|
||||
}
|
||||
if (durationMs < 1000) {
|
||||
const durationMsFormatted = Math.floor(durationMs);
|
||||
return short
|
||||
? `${durationMsFormatted} ms`
|
||||
: `${durationMsFormatted} milliseconds`;
|
||||
}
|
||||
const seconds = Math.floor(durationMs / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
if (seconds < 60) {
|
||||
return short ? `${seconds} s` : `${seconds} seconds`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
return short
|
||||
? `${minutes} min ${
|
||||
remainingSeconds > 0 ? `${remainingSeconds} s` : ''
|
||||
}`
|
||||
: `${minutes} minutes${
|
||||
remainingSeconds > 0 ? ` ${remainingSeconds} seconds` : ''
|
||||
}`;
|
||||
}
|
||||
return short ? `${seconds} s` : `${seconds} seconds`;
|
||||
},
|
||||
urlIsNotLocalhostOrIp(url: string): boolean {
|
||||
const parsed = new URL(url);
|
||||
if (
|
||||
parsed.hostname === 'localhost' ||
|
||||
parsed.hostname === '127.0.0.1' ||
|
||||
parsed.hostname === '::1'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const ipv4Regex = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(parsed.hostname)) {
|
||||
return false;
|
||||
}
|
||||
return parsed.protocol === 'https:';
|
||||
},
|
||||
};
|
||||
|
||||
export const validationUtils = {
|
||||
isValidationError: (
|
||||
error: unknown,
|
||||
): error is AxiosError<{ code?: string; params?: { message?: string } }> => {
|
||||
console.error('isValidationError', error);
|
||||
return (
|
||||
error instanceof AxiosError &&
|
||||
error.response?.status === 409 &&
|
||||
error.response?.data?.code === 'VALIDATION'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
|
||||
const innerRef = useRef<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) return;
|
||||
if (typeof ref === 'function') {
|
||||
ref(innerRef.current);
|
||||
} else {
|
||||
ref.current = innerRef.current;
|
||||
}
|
||||
});
|
||||
|
||||
return innerRef;
|
||||
}
|
||||
|
||||
export const localesMap = {
|
||||
[LocalesEnum.CHINESE_SIMPLIFIED]: '简体中文',
|
||||
[LocalesEnum.GERMAN]: 'Deutsch',
|
||||
[LocalesEnum.ENGLISH]: 'English',
|
||||
[LocalesEnum.SPANISH]: 'Español',
|
||||
[LocalesEnum.FRENCH]: 'Français',
|
||||
[LocalesEnum.JAPANESE]: '日本語',
|
||||
[LocalesEnum.DUTCH]: 'Nederlands',
|
||||
[LocalesEnum.PORTUGUESE]: 'Português',
|
||||
[LocalesEnum.CHINESE_TRADITIONAL]: '繁體中文',
|
||||
};
|
||||
|
||||
export const useElementSize = (ref: RefObject<HTMLElement>) => {
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
useEffect(() => {
|
||||
const handleResize = (entries: ResizeObserverEntry[]) => {
|
||||
if (entries[0]) {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
setSize({ width, height });
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
|
||||
if (ref.current) {
|
||||
resizeObserver.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [ref.current]);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
export const isStepFileUrl = (json: unknown): json is string => {
|
||||
return (
|
||||
Boolean(json) &&
|
||||
typeof json === 'string' &&
|
||||
(json.includes('/api/v1/step-files/') || json.includes('file://'))
|
||||
);
|
||||
};
|
||||
|
||||
export const useTimeAgo = (date: Date) => {
|
||||
const [timeAgo, setTimeAgo] = useState(() =>
|
||||
formatUtils.formatDateToAgo(date),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const updateInterval = () => {
|
||||
const now = dayjs();
|
||||
const inputDate = dayjs(date);
|
||||
const diffInSeconds = now.diff(inputDate, 'second');
|
||||
|
||||
// Update every second if less than a minute
|
||||
// Update every minute if less than an hour
|
||||
// Update every hour if less than a day
|
||||
// Update every day if more than a day
|
||||
if (diffInSeconds < 60) return 1000;
|
||||
if (diffInSeconds < 3600) return 60000;
|
||||
if (diffInSeconds < 86400) return 3600000;
|
||||
return 86400000;
|
||||
};
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setTimeAgo(formatUtils.formatDateToAgo(date));
|
||||
}, updateInterval());
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [date]);
|
||||
|
||||
return timeAgo;
|
||||
};
|
||||
|
||||
export const determineDefaultRoute = (
|
||||
checkAccess: (permission: Permission) => boolean,
|
||||
) => {
|
||||
if (checkAccess(Permission.READ_FLOW)) {
|
||||
return authenticationSession.appendProjectRoutePrefix('/flows');
|
||||
}
|
||||
if (checkAccess(Permission.READ_RUN)) {
|
||||
return authenticationSession.appendProjectRoutePrefix('/runs');
|
||||
}
|
||||
return authenticationSession.appendProjectRoutePrefix('/settings');
|
||||
};
|
||||
export const NEW_FLOW_QUERY_PARAM = 'newFlow';
|
||||
export const NEW_TABLE_QUERY_PARAM = 'newTable';
|
||||
export const NEW_MCP_QUERY_PARAM = 'newMcp';
|
||||
export const parentWindow: Window = window.opener ?? window.parent;
|
||||
export const cleanLeadingSlash = (url: string) => {
|
||||
return url.startsWith('/') ? url.slice(1) : url;
|
||||
};
|
||||
|
||||
export const cleanTrailingSlash = (url: string) => {
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
export const combinePaths = ({
|
||||
firstPath,
|
||||
secondPath,
|
||||
}: {
|
||||
firstPath: string;
|
||||
secondPath: string;
|
||||
}) => {
|
||||
const cleanedFirstPath = cleanTrailingSlash(firstPath);
|
||||
const cleanedSecondPath = cleanLeadingSlash(secondPath);
|
||||
return `${cleanedFirstPath}/${cleanedSecondPath}`;
|
||||
};
|
||||
|
||||
const getBlobType = (extension: 'json' | 'txt' | 'csv') => {
|
||||
switch (extension) {
|
||||
case 'csv':
|
||||
return 'text/csv';
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'txt':
|
||||
return 'text/plain';
|
||||
default:
|
||||
return `text/plain`;
|
||||
}
|
||||
};
|
||||
|
||||
type downloadFileProps =
|
||||
| {
|
||||
obj: string;
|
||||
fileName: string;
|
||||
extension: 'json' | 'txt' | 'csv';
|
||||
}
|
||||
| {
|
||||
obj: JSZip;
|
||||
fileName: string;
|
||||
extension: 'zip';
|
||||
};
|
||||
export const downloadFile = async ({
|
||||
obj,
|
||||
fileName,
|
||||
extension,
|
||||
}: downloadFileProps) => {
|
||||
const blob =
|
||||
extension === 'zip'
|
||||
? await obj.generateAsync({ type: 'blob' })
|
||||
: //utf-8 with bom
|
||||
new Blob([new Uint8Array([0xef, 0xbb, 0xbf]), obj], {
|
||||
type: getBlobType(extension),
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${fileName}.${extension}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const wait = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
export const scrollToElementAndClickIt = (elementId: string) => {
|
||||
const element = document.getElementById(elementId);
|
||||
element?.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'start',
|
||||
});
|
||||
element?.click();
|
||||
};
|
||||
|
||||
export const routesThatRequireProjectId = {
|
||||
runs: '/runs',
|
||||
singleRun: '/runs/:runId',
|
||||
flows: '/flows',
|
||||
singleFlow: '/flows/:flowId',
|
||||
connections: '/connections',
|
||||
singleConnection: '/connections/:connectionId',
|
||||
tables: '/tables',
|
||||
singleTable: '/tables/:tableId',
|
||||
todos: '/todos',
|
||||
singleTodo: '/todos/:todoId',
|
||||
settings: '/settings',
|
||||
releases: '/releases',
|
||||
singleRelease: '/releases/:releaseId',
|
||||
};
|
||||
Reference in New Issue
Block a user