-
{children}
+
+
+
+
+ {!isOpen && (
+
+ )}
+
-
+
+
+
+
>
);
diff --git a/components/terminus/Blockly/BlocklyComponent.module.css b/components/terminus/Blockly/BlocklyComponent.module.css
index dfc9bf3ed..81a41cee6 100644
--- a/components/terminus/Blockly/BlocklyComponent.module.css
+++ b/components/terminus/Blockly/BlocklyComponent.module.css
@@ -1,7 +1,7 @@
.blocklyDiv {
height: calc(100% - 151px);
- width: calc(100% - 256px);
+ width: calc(100%);
position: absolute;
top: 151px;
- left: 256px;
+ /* left: 256px; */
}
diff --git a/components/terminus/Blockly/BlocklyComponent.tsx b/components/terminus/Blockly/BlocklyComponent.tsx
index b53901009..a5e95c675 100644
--- a/components/terminus/Blockly/BlocklyComponent.tsx
+++ b/components/terminus/Blockly/BlocklyComponent.tsx
@@ -6,8 +6,8 @@ import { useTranslation } from 'next-i18next';
import * as Blockly from 'blockly/core';
import 'blockly/blocks';
import { maskSetup } from '@components/terminus/blocks/customblocks';
-import * as locale from 'blockly/msg/en';
-Blockly.setLocale(locale);
+// import * as locale from 'blockly/msg/en';
+// Blockly.setLocale(locale);
import { generateModel } from '@components/terminus/blocks/generator';
import { errorToast, successToast } from '@components/Toaster';
@@ -39,7 +39,7 @@ function BlocklyComponent(props) {
const body = {
model: model,
- blockly_model: domToPretty,
+ blocklyModel: domToPretty,
};
const requestOptions = {
@@ -84,6 +84,14 @@ function BlocklyComponent(props) {
const { initialXml, ...rest } = props;
primaryWorkspace.current = Blockly.inject(blocklyDiv.current as any, {
toolbox: toolbox.current,
+ zoom: {
+ controls: true,
+ wheel: true,
+ maxScale: 3,
+ minScale: 0.3,
+ scaleSpeed: 1.2,
+ startScale: 0.8,
+ },
readOnly: false,
trashcan: true,
media: '/terminus/',
diff --git a/components/terminus/policies/AddPolicyForm.tsx b/components/terminus/policies/AddPolicyForm.tsx
new file mode 100644
index 000000000..d36c65aa0
--- /dev/null
+++ b/components/terminus/policies/AddPolicyForm.tsx
@@ -0,0 +1,540 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Card, LinkBack, PageHeader } from '@boxyhq/internal-ui';
+import { CheckSquare, Info, Square, Search, X } from 'lucide-react';
+import { useTranslation } from 'next-i18next';
+import { errorToast, successToast } from '@components/Toaster';
+import router from 'next/router';
+import { Button } from 'react-daisyui';
+import { descriptions, PII_POLICY, SupportedLanguages } from '@components/terminus/policies/types';
+import CodeEditor from './CodeEditor';
+
+type entityState = {
+ type: string;
+ description: string;
+ region: string;
+};
+
+type formState = {
+ piiPolicy: string;
+ product: string;
+ language: string;
+ piiEntities: Array
;
+ selectedRegions: Array;
+ accessControlPolicy: string;
+};
+
+const initialState = {
+ piiPolicy: '',
+ product: '',
+ language: '',
+ piiEntities: [],
+ selectedRegions: [],
+ accessControlPolicy: '',
+};
+
+const AddPolicyForm = () => {
+ const { t } = useTranslation('common');
+ const [initialEntities, setInitialEntities] = useState>([]);
+ const [regions, setRegions] = useState>([]);
+ const [formData, setFormData] = useState(initialState);
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+ const [loading, setLoading] = useState(false);
+ const [hoveredEntity, setHoveredEntity] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [expandedRegions, setExpandedRegions] = useState({});
+ const [showAllEntities, setShowAllEntities] = useState(false);
+
+ const languages = Object.keys(SupportedLanguages);
+ const policies = Object.values(PII_POLICY).filter((value) => value !== 'None');
+
+ useEffect(() => {
+ (async function () {
+ const entitiesResp = await fetch(`/api/admin/llm-vault/policies/entities`);
+ const entitiesObject = (await entitiesResp.json())?.data;
+
+ const convertedIdentifiers: Array = [];
+ if (entitiesObject) {
+ const regionList = Object.keys(entitiesObject);
+
+ for (const [region, types] of Object.entries(entitiesObject)) {
+ (types as Array).forEach((type) => {
+ convertedIdentifiers.push({
+ type: type,
+ description: getDescription(type),
+ region: region,
+ });
+ });
+ }
+
+ setInitialEntities(convertedIdentifiers);
+ setRegions(regionList);
+ }
+ })();
+ }, []);
+
+ useEffect(() => {
+ showAllRegions();
+ }, [initialEntities]);
+
+ const getCleanedType = (type: string) => {
+ const cleanedType = type.replace(/^AU_|^IN_|^SG_|^UK_|^US_/, '');
+ return cleanedType;
+ };
+
+ const getDescription = (type) => {
+ return descriptions[type] || 'No description available.';
+ };
+
+ const filteredEntities = useMemo(() => {
+ if (!searchQuery) return initialEntities;
+ const query = searchQuery.toLowerCase();
+ return initialEntities.filter((entity) => entity.type.toLowerCase().includes(query));
+ }, [searchQuery, initialEntities]);
+
+ const areAllFilteredEntitiesSelected = filteredEntities.every((entity) =>
+ formData.piiEntities.some((e) => e.type === entity.type)
+ );
+
+ const toggleAllFilteredEntities = () => {
+ setFormData((prev) => {
+ if (areAllFilteredEntitiesSelected) {
+ // Remove all filtered entities
+ return {
+ ...prev,
+ piiEntities: prev.piiEntities.filter(
+ (selected) => !filteredEntities.some((filtered) => filtered.type === selected.type)
+ ),
+ selectedRegions: [],
+ };
+ } else {
+ // Add all filtered entities that aren't already selected
+ const currentSelected = new Set(prev.piiEntities.map((e) => e.type));
+ const newEntities = filteredEntities.filter((entity) => !currentSelected.has(entity.type));
+ return {
+ ...prev,
+ piiEntities: [...prev.piiEntities, ...newEntities],
+ selectedRegions: regions,
+ };
+ }
+ });
+ };
+
+ const selectCount = () => {
+ return `${formData.piiEntities.length} of ${initialEntities.length} entities selected`;
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const setAccessControlPolicy = (value) => {
+ setFormData((prev) => ({
+ ...prev,
+ accessControlPolicy: value,
+ }));
+ };
+
+ const getSelectedRegions = (entities) => {
+ const selected_regions: Array = [];
+ regions.forEach((region) => {
+ if (entities.some((e) => e.region === region)) {
+ selected_regions.push(region);
+ }
+ });
+ return selected_regions.length > 0 ? selected_regions : [];
+ };
+
+ const onEntityChange = (entity) => {
+ setFormData((prev) => {
+ const isSelected = prev.piiEntities.some((e) => e.type === entity.type);
+ const newPIIEntities = isSelected
+ ? prev.piiEntities.filter((e) => e.type !== entity.type)
+ : [...prev.piiEntities, entity];
+ const newSelectedRegions = getSelectedRegions(newPIIEntities);
+ return {
+ ...prev,
+ piiEntities: newPIIEntities,
+ selectedRegions: newSelectedRegions,
+ };
+ });
+ };
+
+ const toggleRegion = (region) => {
+ setFormData((prev) => {
+ const isSelected = prev.selectedRegions.some((e) => e === region);
+ return {
+ ...prev,
+ piiEntities: isSelected
+ ? prev.piiEntities.filter((e) => e.region !== region)
+ : [...prev.piiEntities, ...initialEntities.filter((e) => e.region === region)],
+ selectedRegions: isSelected
+ ? prev.selectedRegions.filter((e) => e !== region)
+ : [...prev.selectedRegions, region],
+ };
+ });
+ };
+
+ const showAllRegions = () => {
+ if (showAllEntities) {
+ regions.forEach((region) => {
+ showRegionCustom(region, false);
+ });
+ setShowAllEntities(false);
+ } else {
+ regions.forEach((region) => {
+ showRegionCustom(region, true);
+ });
+ setShowAllEntities(true);
+ }
+ };
+
+ const validateForm = (formData: formState) => {
+ const errors: { [key: string]: string } = {};
+
+ if (!formData.piiPolicy) {
+ errors.piiPolicy = 'PII Policy is required.';
+ }
+
+ if (!formData.product) {
+ errors.product = 'Product is required.';
+ }
+
+ if (!formData.language) {
+ errors.language = 'Language is required.';
+ }
+
+ if (formData.piiEntities.length === 0) {
+ errors.piiEntities = 'At least one PII entity must be selected.';
+ }
+
+ if (formData.selectedRegions.length === 0) {
+ errors.selectedRegions = 'At least one region must be selected.';
+ }
+
+ if (!formData.accessControlPolicy) {
+ errors.accessControlPolicy = 'Access Control Policy is required.';
+ }
+
+ return errors;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ const validationErrors = validateForm(formData);
+
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ const response = await fetch(`/api/admin/llm-vault/policies/${formData.product}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ product: formData.product,
+ piiPolicy: formData.piiPolicy,
+ language: SupportedLanguages[formData.language],
+ piiEntities: formData.piiEntities.map((e) => e.type).toString(),
+ accessControlPolicy: Buffer.from(formData.accessControlPolicy, 'utf-8').toString('base64'),
+ }),
+ });
+
+ if (response.ok) {
+ successToast(t('llm_policy_saved_success_toast'));
+ setFormData(initialState);
+ router.push('/admin/llm-vault/policies');
+ }
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ } catch (err: any) {
+ errorToast(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const showRegion = (region) => {
+ setExpandedRegions((prev) => {
+ const newExpandedState = {
+ ...prev,
+ [region]: prev[region] !== undefined ? !prev[region] : true,
+ };
+ setShowAllEntities(Object.values(newExpandedState).every((value) => value === true));
+ return newExpandedState;
+ });
+ };
+
+ const showRegionCustom = (region, value) => {
+ setExpandedRegions((prev) => {
+ const newExpandedState = {
+ ...prev,
+ [region]: value,
+ };
+
+ return newExpandedState;
+ });
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default AddPolicyForm;
diff --git a/components/terminus/policies/CodeEditor.tsx b/components/terminus/policies/CodeEditor.tsx
new file mode 100644
index 000000000..466fcef99
--- /dev/null
+++ b/components/terminus/policies/CodeEditor.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Editor } from '@monaco-editor/react';
+
+const CodeEditor = ({ code, setCode }: { code: string; setCode: (value: string | undefined) => void }) => {
+ return (
+
+
+
+ );
+};
+
+export default CodeEditor;
diff --git a/components/terminus/policies/EditPolicyForm.tsx b/components/terminus/policies/EditPolicyForm.tsx
new file mode 100644
index 000000000..783dfea18
--- /dev/null
+++ b/components/terminus/policies/EditPolicyForm.tsx
@@ -0,0 +1,591 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Card, LinkBack, PageHeader } from '@boxyhq/internal-ui';
+import { CheckSquare, Info, Square, Search, X } from 'lucide-react';
+import { useTranslation } from 'next-i18next';
+import { errorToast, successToast } from '@components/Toaster';
+import { Button } from 'react-daisyui';
+import {
+ descriptions,
+ LanguageKey,
+ PII_POLICY,
+ SupportedLanguages,
+} from '@components/terminus/policies/types';
+import CodeEditor from './CodeEditor';
+
+type entityState = {
+ type: string;
+ description: string;
+ region: string;
+};
+
+type formState = {
+ piiPolicy: string;
+ product: string;
+ language: string;
+ piiEntities: Array;
+ selectedRegions: Array;
+ accessControlPolicy: string;
+};
+
+const initialState = {
+ piiPolicy: '',
+ product: '',
+ language: '',
+ piiEntities: [],
+ selectedRegions: [],
+ accessControlPolicy: '',
+};
+
+type editFormProps = {
+ piiPolicy: string;
+ product: string;
+ language: string;
+ piiEntities: string;
+ accessControlPolicy: string;
+};
+
+const EditPolicyForm = ({
+ piiPolicy,
+ product,
+ language,
+ piiEntities,
+ accessControlPolicy,
+}: editFormProps) => {
+ const { t } = useTranslation('common');
+ const [initialEntities, setInitialEntities] = useState>([]);
+ const [regions, setRegions] = useState>([]);
+ const [formData, setFormData] = useState(initialState);
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+ const [loading, setLoading] = useState(false);
+ const [hoveredEntity, setHoveredEntity] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [expandedRegions, setExpandedRegions] = useState({});
+ const [showAllEntities, setShowAllEntities] = useState(false);
+
+ const languages = Object.keys(SupportedLanguages);
+ const policies = Object.values(PII_POLICY).filter((value) => value !== 'None');
+
+ useEffect(() => {
+ (async function () {
+ const entitiesResp = await fetch(`/api/admin/llm-vault/policies/entities`);
+ const entitiesList = (await entitiesResp.json())?.data;
+
+ const convertedIdentifiers: Array = [];
+ const regionList = Object.keys(entitiesList);
+
+ for (const [region, types] of Object.entries(entitiesList)) {
+ (types as Array).forEach((type) => {
+ convertedIdentifiers.push({
+ type: type,
+ description: getDescription(type),
+ region: region,
+ });
+ });
+ }
+
+ setInitialEntities(convertedIdentifiers);
+ setRegions(regionList);
+ })();
+ }, []);
+
+ const getRegionByType = (type) => {
+ const identifier = initialEntities.find((item) => item.type === type);
+
+ return identifier ? identifier.region : '';
+ };
+
+ const LANGUAGE_CODE_MAP: { [key: string]: LanguageKey } = Object.entries(SupportedLanguages).reduce(
+ (acc, [key, value]) => {
+ acc[value] = key as LanguageKey;
+ return acc;
+ },
+ {} as { [key: string]: LanguageKey }
+ );
+
+ useEffect(() => {
+ showAllRegions();
+ const convertedIdentifiers: Array = [];
+
+ piiEntities.split(',').forEach((type) =>
+ convertedIdentifiers.push({
+ type: type,
+ description: getDescription(type),
+ region: getRegionByType(type),
+ })
+ );
+
+ const preSelectedRegions = getSelectedRegions(convertedIdentifiers);
+
+ setFormData({
+ piiPolicy,
+ product,
+ language: LANGUAGE_CODE_MAP[language],
+ piiEntities: convertedIdentifiers,
+ selectedRegions: preSelectedRegions,
+ accessControlPolicy: accessControlPolicy
+ ? Buffer.from(accessControlPolicy, 'base64').toString('utf-8')
+ : '',
+ });
+ }, [initialEntities]);
+
+ const getCleanedType = (type: string) => {
+ const cleanedType = type.replace(/^AU_|^IN_|^SG_|^UK_|^US_/, '');
+ return cleanedType;
+ };
+
+ const getDescription = (type) => {
+ return descriptions[type] || 'No description available.';
+ };
+
+ const filteredEntities = useMemo(() => {
+ if (!searchQuery) return initialEntities;
+ const query = searchQuery.toLowerCase();
+ return initialEntities.filter((entity) => entity.type.toLowerCase().includes(query));
+ }, [searchQuery, initialEntities]);
+
+ const areAllFilteredEntitiesSelected = filteredEntities.every((entity) =>
+ formData.piiEntities.some((e) => e.type === entity.type)
+ );
+
+ const toggleAllFilteredEntities = () => {
+ setFormData((prev) => {
+ if (areAllFilteredEntitiesSelected) {
+ // Remove all filtered entities
+ return {
+ ...prev,
+ piiEntities: prev.piiEntities.filter(
+ (selected) => !filteredEntities.some((filtered) => filtered.type === selected.type)
+ ),
+ selectedRegions: [],
+ };
+ } else {
+ // Add all filtered entities that aren't already selected
+ const currentSelected = new Set(prev.piiEntities.map((e) => e.type));
+ const newEntities = filteredEntities.filter((entity) => !currentSelected.has(entity.type));
+ return {
+ ...prev,
+ piiEntities: [...prev.piiEntities, ...newEntities],
+ selectedRegions: regions,
+ };
+ }
+ });
+ };
+
+ const selectCount = () => {
+ return `${formData.piiEntities.length} of ${initialEntities.length} entities selected`;
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const setAccessControlPolicy = (value) => {
+ setFormData((prev) => ({
+ ...prev,
+ accessControlPolicy: value,
+ }));
+ };
+
+ const getSelectedRegions = (entities) => {
+ const selected_regions: Array = [];
+ regions.forEach((region) => {
+ if (entities.some((e) => e.region === region)) {
+ selected_regions.push(region);
+ }
+ });
+ return selected_regions.length > 0 ? selected_regions : [];
+ };
+
+ const onEntityChange = (entity) => {
+ setFormData((prev) => {
+ const isSelected = prev.piiEntities.some((e) => e.type === entity.type);
+ const newPIIEntities = isSelected
+ ? prev.piiEntities.filter((e) => e.type !== entity.type)
+ : [...prev.piiEntities, entity];
+ const newSelectedRegions = getSelectedRegions(newPIIEntities);
+ return {
+ ...prev,
+ piiEntities: newPIIEntities,
+ selectedRegions: newSelectedRegions,
+ };
+ });
+ };
+
+ const toggleRegion = (region) => {
+ setFormData((prev) => {
+ const isSelected = prev.selectedRegions.some((e) => e === region);
+ return {
+ ...prev,
+ piiEntities: isSelected
+ ? prev.piiEntities.filter((e) => e.region !== region)
+ : [...prev.piiEntities, ...initialEntities.filter((e) => e.region === region)],
+ selectedRegions: isSelected
+ ? prev.selectedRegions.filter((e) => e !== region)
+ : [...prev.selectedRegions, region],
+ };
+ });
+ };
+
+ const showAllRegions = () => {
+ if (showAllEntities) {
+ regions.forEach((region) => {
+ showRegionCustom(region, false);
+ });
+ setShowAllEntities(false);
+ } else {
+ regions.forEach((region) => {
+ showRegionCustom(region, true);
+ });
+ setShowAllEntities(true);
+ }
+ };
+
+ const validateForm = (formData: formState) => {
+ const errors: { [key: string]: string } = {};
+
+ if (!formData.piiPolicy) {
+ errors.piiPolicy = 'PII Policy is required.';
+ }
+
+ if (!formData.product) {
+ errors.product = 'Product is required.';
+ }
+
+ if (!formData.language) {
+ errors.language = 'Language is required.';
+ }
+
+ if (formData.piiEntities.length === 0) {
+ errors.piiEntities = 'At least one PII entity must be selected.';
+ }
+
+ if (formData.selectedRegions.length === 0) {
+ errors.selectedRegions = 'At least one region must be selected.';
+ }
+
+ if (!formData.accessControlPolicy) {
+ errors.accessControlPolicy = 'Access Control Policy is required.';
+ }
+
+ return errors;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ const validationErrors = validateForm(formData);
+
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ const response = await fetch(`/api/admin/llm-vault/policies/${formData.product}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ product: formData.product,
+ piiPolicy: formData.piiPolicy,
+ piiEntities: formData.piiEntities.map((e) => e.type).toString(),
+ language: SupportedLanguages[formData.language],
+ accessControlPolicy: Buffer.from(formData.accessControlPolicy, 'utf-8').toString('base64'),
+ }),
+ });
+
+ if (response.ok) {
+ successToast(t('llm_policy_update_success_toast'));
+ }
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ } catch (err: any) {
+ errorToast(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const showRegion = (region) => {
+ setExpandedRegions((prev) => {
+ const newExpandedState = {
+ ...prev,
+ [region]: prev[region] !== undefined ? !prev[region] : true,
+ };
+ setShowAllEntities(Object.values(newExpandedState).every((value) => value === true));
+ return newExpandedState;
+ });
+ };
+
+ const showRegionCustom = (region, value) => {
+ setExpandedRegions((prev) => {
+ const newExpandedState = {
+ ...prev,
+ [region]: value,
+ };
+
+ return newExpandedState;
+ });
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default EditPolicyForm;
diff --git a/components/terminus/policies/types.ts b/components/terminus/policies/types.ts
new file mode 100644
index 000000000..1421c4b12
--- /dev/null
+++ b/components/terminus/policies/types.ts
@@ -0,0 +1,89 @@
+export const PII_POLICY_OPTIONS = [
+ 'none',
+ 'detect_mask',
+ 'detect_redact',
+ 'detect_report',
+ 'detect_block',
+] as const;
+
+export const PII_POLICY: {
+ [key in (typeof PII_POLICY_OPTIONS)[number]]: string;
+} = {
+ none: 'None',
+ detect_mask: 'Detect & Mask',
+ detect_redact: 'Detect & Redact',
+ detect_report: 'Detect & Report',
+ detect_block: 'Detect & Block',
+} as const;
+
+export enum SupportedLanguages {
+ English = 'en',
+ Spanish = 'es',
+ German = 'de',
+}
+
+export type LanguageKey = keyof typeof SupportedLanguages;
+
+type DescriptionKey =
+ | 'AU_ABN'
+ | 'AU_ACN'
+ | 'AU_TFN'
+ | 'AU_MEDICARE'
+ | 'IN_AADHAAR'
+ | 'IN_PAN'
+ | 'IN_PASSPORT'
+ | 'IN_VOTER'
+ | 'IN_VEHICLE_REGISTRATION'
+ | 'SG_NRIC_FIN'
+ | 'UK_NHS'
+ | 'US_ITIN'
+ | 'US_PASSPORT'
+ | 'US_SSN'
+ | 'US_BANK_NUMBER'
+ | 'US_DRIVER_LICENSE'
+ | 'IP_ADDRESS'
+ | 'IBAN_CODE'
+ | 'NRP'
+ | 'CREDIT_CARD'
+ | 'URL'
+ | 'LOCATION'
+ | 'EMAIL_ADDRESS'
+ | 'MEDICAL_LICENSE'
+ | 'PERSON'
+ | 'DATE_TIME'
+ | 'PHONE_NUMBER'
+ | 'ORGANIZATION'
+ | 'CRYPTO';
+
+// Create a constant object with descriptions
+export const descriptions: Record = {
+ AU_ABN: 'Australian Business Number, a unique identifier for businesses in Australia.',
+ AU_ACN: 'Australian Company Number, a unique identifier for companies in Australia.',
+ AU_TFN: 'Tax File Number, used for tax purposes in Australia.',
+ AU_MEDICARE: 'Medicare card number for health services in Australia.',
+ IN_AADHAAR: 'Aadhaar number, a unique identification number issued in India.',
+ IN_PAN: 'Permanent Account Number, used for tax identification in India.',
+ IN_PASSPORT: 'Passport number for international travel from India.',
+ IN_VOTER: 'Voter ID number used for electoral purposes in India.',
+ IN_VEHICLE_REGISTRATION: 'Vehicle registration number in India.',
+ SG_NRIC_FIN: 'National Registration Identity Card/Foreign Identification Number in Singapore.',
+ UK_NHS: 'National Health Service number in the United Kingdom.',
+ US_ITIN: 'Individual Taxpayer Identification Number issued by the IRS.',
+ US_PASSPORT: 'Passport number for international travel issued by the U.S.',
+ US_SSN: 'Social Security Number format: XXX-XX-XXXX',
+ US_BANK_NUMBER: 'Bank account numbers used for transactions.',
+ US_DRIVER_LICENSE: "Driver's license numbers issued by U.S. states.",
+ IP_ADDRESS: 'An identifier for a device on a TCP/IP network.',
+ IBAN_CODE: 'International Bank Account Number, used to identify bank accounts internationally.',
+ NRP: 'National Registration Profile, used for various identification purposes.',
+ CREDIT_CARD: 'A card issued by a financial institution allowing the holder to borrow funds.',
+ URL: 'Uniform Resource Locator, used to specify addresses on the internet.',
+ LOCATION: 'Geographical coordinates or address indicating a specific place.',
+ EMAIL_ADDRESS: 'A unique identifier for an email account.',
+ MEDICAL_LICENSE: 'License required to practice medicine legally.',
+ PERSON: 'An individual human being.',
+ DATE_TIME: 'A representation of date and time.',
+ PHONE_NUMBER: 'A sequence of digits assigned to a telephone line.',
+ ORGANIZATION: 'A group of people organized for a particular purpose.',
+ CRYPTO: 'Refers to cryptocurrencies or cryptographic assets.',
+} as const;
diff --git a/e2e/support/fixtures/setuplink-ds-page.ts b/e2e/support/fixtures/setuplink-ds-page.ts
index a578cf28a..daf204a5a 100644
--- a/e2e/support/fixtures/setuplink-ds-page.ts
+++ b/e2e/support/fixtures/setuplink-ds-page.ts
@@ -68,7 +68,7 @@ export class SetupLinkDSPage {
await expect(this.page.getByRole('table')).toBeVisible();
// Delete the created setuplink
- await this.page.getByRole('button').nth(5).click();
+ await this.page.getByRole('button').nth(6).click();
await this.page.getByRole('button', { name: 'Delete' }).click();
}
}
diff --git a/e2e/support/fixtures/setuplink-page.ts b/e2e/support/fixtures/setuplink-page.ts
index 81cacc7ed..e783e0fb0 100644
--- a/e2e/support/fixtures/setuplink-page.ts
+++ b/e2e/support/fixtures/setuplink-page.ts
@@ -76,7 +76,7 @@ export class SetupLinkPage {
await expect(this.page.getByRole('table')).toBeVisible();
// Delete the created setuplink
- await this.page.getByRole('button').nth(5).click();
+ await this.page.getByRole('button').nth(6).click();
await this.page.getByRole('button', { name: 'Delete' }).click();
}
}
diff --git a/e2e/ui/Directory Sync/setup_link_ds.spec.ts b/e2e/ui/Directory Sync/setup_link_ds.spec.ts
index 1cebc4e61..b929004e6 100644
--- a/e2e/ui/Directory Sync/setup_link_ds.spec.ts
+++ b/e2e/ui/Directory Sync/setup_link_ds.spec.ts
@@ -43,7 +43,7 @@ async function deleteDirectory(setupLinkPage: Page) {
await setupLinkPage.getByRole('button', { name: 'Confirm' }).click();
}
-test.describe('Admin Portal Dyrectory Sync SetupLink', () => {
+test.describe('Admin Portal Directory Sync SetupLink', () => {
test('should be able to create setup link and directories', async ({ page, setuplinkPage }) => {
// get setuplink url
const linkContent = await setuplinkPage.getSetupLinkUrl();
diff --git a/ee/terminus/pages/audit-logs.tsx b/ee/terminus/pages/audit-logs.tsx
new file mode 100644
index 000000000..4948da475
--- /dev/null
+++ b/ee/terminus/pages/audit-logs.tsx
@@ -0,0 +1,111 @@
+import type { NextPage } from 'next';
+import dynamic from 'next/dynamic';
+import { useEffect, useState } from 'react';
+import { useProject, useGroups } from '@lib/ui/retraced';
+import { LinkBack, Loading, Error } from '@boxyhq/internal-ui';
+import { Select } from 'react-daisyui';
+import { useTranslation } from 'next-i18next';
+import LicenseRequired from '@components/LicenseRequired';
+
+const LogsViewer = dynamic(() => import('@components/retraced/LogsViewer'), {
+ ssr: false,
+});
+
+interface Props {
+ host?: string;
+ projectId: string;
+ hasValidLicense: boolean;
+}
+
+const Events: NextPage = ({ host, projectId, hasValidLicense }: Props) => {
+ const { t } = useTranslation('common');
+
+ const [environment, setEnvironment] = useState('');
+ const [group, setGroup] = useState('');
+
+ const { project, isLoading, isError } = useProject(projectId);
+ const { groups } = useGroups(projectId, environment);
+
+ // Set the environment
+ useEffect(() => {
+ if (project) {
+ setEnvironment(project.environments[0]?.id);
+ }
+ }, [project]);
+
+ // Set the group
+ useEffect(() => {
+ if (groups && groups.length > 0) {
+ setGroup(groups[0].group_id);
+ }
+ }, [groups]);
+
+ if (!hasValidLicense) {
+ return ;
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (isError) {
+ return ;
+ }
+
+ const displayLogsViewer = project && environment && group;
+
+ return (
+
+
+
+
{project?.name}
+
+
+
+
+ {project ? (
+
+ ) : null}
+
+
+
+ {groups ? (
+
+ ) : null}
+
+
+
+ {displayLogsViewer && (
+
+ )}
+
+
+ );
+};
+
+export default Events;
diff --git a/internal-ui/package-lock.json b/internal-ui/package-lock.json
index 3a7255559..3bd749903 100644
--- a/internal-ui/package-lock.json
+++ b/internal-ui/package-lock.json
@@ -30,9 +30,9 @@
},
"peerDependencies": {
"@boxyhq/react-ui": ">=3.3.42",
- "@heroicons/react": ">=2.1.1",
"classnames": ">=2.5.1",
"formik": ">=2.4.5",
+ "lucide-react": ">=0.461.0",
"next": ">=14.1.0",
"next-i18next": ">=13.3.0",
"prismjs": ">=1.29.0",
@@ -1841,9 +1841,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001684",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
- "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
+ "version": "1.0.30001669",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
+ "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
"dev": true,
"funding": [
{
@@ -1959,9 +1959,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.65",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz",
- "integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
+ "version": "1.5.42",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.42.tgz",
+ "integrity": "sha512-gIfKavKDw1mhvic9nbzA5lZw8QSHpdMwLwXc0cWidQz9B15pDoDdDH4boIatuFfeoCatb3a/NGL6CYRVFxGZ9g==",
"dev": true,
"license": "ISC"
},
@@ -2758,9 +2758,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
@@ -3049,9 +3049,9 @@
}
},
"node_modules/rollup": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz",
- "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==",
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
+ "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/internal-ui/package.json b/internal-ui/package.json
index 24beece6b..875575661 100644
--- a/internal-ui/package.json
+++ b/internal-ui/package.json
@@ -43,8 +43,8 @@
"@rollup/rollup-linux-x64-gnu": "4.28.0"
},
"peerDependencies": {
+ "lucide-react": ">=0.461.0",
"@boxyhq/react-ui": ">=3.3.42",
- "@heroicons/react": ">=2.1.1",
"classnames": ">=2.5.1",
"formik": ">=2.4.5",
"next": ">=14.1.0",
diff --git a/internal-ui/src/dsync/DirectoryGroups.tsx b/internal-ui/src/dsync/DirectoryGroups.tsx
index f722f7def..af9fb8a5f 100644
--- a/internal-ui/src/dsync/DirectoryGroups.tsx
+++ b/internal-ui/src/dsync/DirectoryGroups.tsx
@@ -1,6 +1,6 @@
import useSWR from 'swr';
import { useTranslation } from 'next-i18next';
-import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
+import { Eye } from 'lucide-react';
import type { ApiSuccess, Group } from '../types';
import { fetcher, addQueryParamsToPath } from '../utils';
import { DirectoryTab } from '../dsync';
@@ -91,7 +91,7 @@ export const DirectoryGroups = ({
{
text: t('bui-shared-view'),
onClick: () => onView?.(group),
- icon: ,
+ icon: ,
},
],
};
diff --git a/internal-ui/src/dsync/DirectoryInfo.tsx b/internal-ui/src/dsync/DirectoryInfo.tsx
index 9effc17d2..4018c4e97 100644
--- a/internal-ui/src/dsync/DirectoryInfo.tsx
+++ b/internal-ui/src/dsync/DirectoryInfo.tsx
@@ -1,5 +1,5 @@
import { useTranslation } from 'next-i18next';
-import ArrowTopRightOnSquareIcon from '@heroicons/react/24/outline/ArrowTopRightOnSquareIcon';
+import { SquareArrowOutUpRight } from 'lucide-react';
import { useDirectory } from '../hooks';
import { DirectoryTab } from '../dsync';
@@ -51,7 +51,7 @@ export const DirectoryInfo = ({
href={`${directory.google_authorization_url}?directoryId=${directory.id}`}
target='_blank'
className='btn-md'
- Icon={ArrowTopRightOnSquareIcon}
+ Icon={SquareArrowOutUpRight}
rel='noopener noreferrer'>
{t('bui-dsync-authorization-google')}
diff --git a/internal-ui/src/dsync/DirectoryUsers.tsx b/internal-ui/src/dsync/DirectoryUsers.tsx
index 9f14bb4b5..7d74d448f 100644
--- a/internal-ui/src/dsync/DirectoryUsers.tsx
+++ b/internal-ui/src/dsync/DirectoryUsers.tsx
@@ -1,6 +1,6 @@
import useSWR from 'swr';
import { useTranslation } from 'next-i18next';
-import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
+import { Eye } from 'lucide-react';
import type { ApiSuccess, User } from '../types';
import { addQueryParamsToPath, fetcher } from '../utils';
import { DirectoryTab } from '../dsync';
@@ -109,7 +109,7 @@ export const DirectoryUsers = ({
{
text: t('bui-shared-view'),
onClick: () => onView?.(user),
- icon: ,
+ icon: ,
},
],
};
diff --git a/internal-ui/src/dsync/DirectoryWebhookLogs.tsx b/internal-ui/src/dsync/DirectoryWebhookLogs.tsx
index c73f140a2..86b71fd00 100644
--- a/internal-ui/src/dsync/DirectoryWebhookLogs.tsx
+++ b/internal-ui/src/dsync/DirectoryWebhookLogs.tsx
@@ -1,7 +1,7 @@
import useSWR from 'swr';
import { useEffect, useState } from 'react';
import { useTranslation } from 'next-i18next';
-import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
+import { Eye } from 'lucide-react';
import { ApiSuccess, type WebhookEventLog } from '../types';
import { fetcher, addQueryParamsToPath } from '../utils';
import { DirectoryTab } from '../dsync';
@@ -116,7 +116,7 @@ export const DirectoryWebhookLogs = ({
{
text: t('bui-shared-view'),
onClick: () => onView?.(event),
- icon: ,
+ icon: ,
},
],
};
diff --git a/internal-ui/src/hooks/index.ts b/internal-ui/src/hooks/index.ts
index fe95ccd87..dbdb05658 100644
--- a/internal-ui/src/hooks/index.ts
+++ b/internal-ui/src/hooks/index.ts
@@ -2,3 +2,4 @@ export { usePaginate } from './usePaginate';
export { useDirectory } from './useDirectory';
export { useRouter } from './useRouter';
export { useFetch, parseResponseContent } from './useFetch';
+export { useAutoResizeTextArea } from './useAutoResizeTextArea';
diff --git a/internal-ui/src/hooks/useAutoResizeTextArea.tsx b/internal-ui/src/hooks/useAutoResizeTextArea.tsx
new file mode 100644
index 000000000..6a43c5133
--- /dev/null
+++ b/internal-ui/src/hooks/useAutoResizeTextArea.tsx
@@ -0,0 +1,14 @@
+import { useRef, useEffect } from 'react';
+
+export function useAutoResizeTextArea() {
+ const textAreaRef = useRef(null);
+
+ useEffect(() => {
+ if (textAreaRef.current) {
+ textAreaRef.current.style.height = '24px';
+ textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
+ }
+ }, [textAreaRef]);
+
+ return textAreaRef;
+}
diff --git a/internal-ui/src/hooks/useFetch.ts b/internal-ui/src/hooks/useFetch.ts
index d4ad32f95..684957a33 100644
--- a/internal-ui/src/hooks/useFetch.ts
+++ b/internal-ui/src/hooks/useFetch.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
type RefetchFunction = () => void;
@@ -24,12 +24,12 @@ export function useFetch({ url }: { url?: string }): {
const [error, setError] = useState(null);
const [refetchIndex, setRefetchIndex] = useState(0);
- const refetch = () => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1);
+ const refetch = useCallback(() => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1), []);
useEffect(() => {
- async function fetchData() {
+ async function fetchData(_url) {
setIsLoading(true);
- const res = await fetch(url!);
+ const res = await fetch(_url);
setIsLoading(false);
const resContent = await parseResponseContent(res);
@@ -44,9 +44,15 @@ export function useFetch({ url }: { url?: string }): {
setError(resContent.error);
}
}
- if (url) {
- fetchData();
+ if (!url) {
+ // Clear states when URL is undefined
+ setData(undefined);
+ setIsLoading(false);
+ setError(null);
+ setRefetchIndex(0);
+ return;
}
+ fetchData(url);
}, [url, refetchIndex]);
return { data, isLoading, error, refetch };
diff --git a/internal-ui/src/identity-federation/AttributesMapping.tsx b/internal-ui/src/identity-federation/AttributesMapping.tsx
index 7df9e2b2c..5f20f23dc 100644
--- a/internal-ui/src/identity-federation/AttributesMapping.tsx
+++ b/internal-ui/src/identity-federation/AttributesMapping.tsx
@@ -1,5 +1,5 @@
import { useTranslation } from 'next-i18next';
-import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
+import { X } from 'lucide-react';
import type { AttributeMapping } from '../types';
const standardAttributes = {
@@ -134,7 +134,7 @@ const AttributeRow = ({