mirror of https://github.com/boxyhq/jackson.git
Refactor security logs configuration components and types
This commit is contained in:
parent
508eec557b
commit
5882bb8309
|
@ -1,178 +1,28 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import type { ApiError, ApiResponse, ApiSuccess } from 'types';
|
||||
import { SinkConfigMapField, getFieldsFromSinkType } from '@lib/sinkConfigMap';
|
||||
import { useRouter } from 'next/router';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { ButtonDanger, ButtonPrimary, ConfirmationModal, LinkBack, Loading } from '@boxyhq/internal-ui';
|
||||
import { SecurityLogsConfigEdit } from 'internal-ui/src/security-logs-config';
|
||||
|
||||
const UpdateApp = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const UpdateConfig = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState<any>({});
|
||||
const [fields, setFields] = useState<SinkConfigMapField[]>([]);
|
||||
|
||||
const { id } = router.query as { id: string };
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SecurityLogsConfig>, ApiError>(
|
||||
`/api/admin/security-logs-config/${id}`,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setConfig(data.data?.config);
|
||||
setFields(getFieldsFromSinkType(data.data?.type) || []);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(`/api/admin/security-logs-config/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config,
|
||||
}),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<SecurityLogsConfig> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
successToast(t('security_logs_config_success'));
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
const urls = {
|
||||
getById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
updateById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
deleteById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
listConfigs: '/admin/settings/security-logs',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href='/admin/settings/security-logs' />
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='mt-5 font-bold text-gray-700 md:text-xl'>{t('security_logs_config_update')}</h2>
|
||||
</div>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='space-y-3'>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<div className='form-control w-full md:w-1/2' key={field.index}>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
className='input-bordered input'
|
||||
id={field.name}
|
||||
placeholder={t(field.placeholder)}
|
||||
value={config[field.name]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('bui-shared-save-changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DeleteApp id={id} />
|
||||
<SecurityLogsConfigEdit id={id} urls={urls} onSuccess={successToast} onError={errorToast} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeleteApp = ({ id }: { id }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const deleteApp = async () => {
|
||||
const rawResponse = await fetch(`/api/admin/security-logs-config/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const response: ApiResponse<unknown> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
successToast(t('security_logs_config_delete_success'));
|
||||
window.location.href = '/admin/settings/security-logs';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='mt-5 flex items-center rounded bg-red-100 p-6 text-red-900'>
|
||||
<div className='flex-1'>
|
||||
<h6 className='mb-1 font-medium'>{t('delete_this_security_logs_config')}</h6>
|
||||
<p className='font-light'>{t('security_logs_wont_be_sent_to_this')}</p>
|
||||
</div>
|
||||
<ButtonDanger
|
||||
type='button'
|
||||
data-modal-toggle='popup-modal'
|
||||
onClick={() => {
|
||||
setDelModalVisible(true);
|
||||
}}>
|
||||
{t('bui-shared-delete')}
|
||||
</ButtonDanger>
|
||||
</section>
|
||||
<ConfirmationModal
|
||||
title={t('delete_the_security_logs_config')}
|
||||
description={t('confirmation_modal_description_config')}
|
||||
visible={delModalVisible}
|
||||
onConfirm={deleteApp}
|
||||
onCancel={() => {
|
||||
setDelModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateApp;
|
||||
export default UpdateConfig;
|
||||
|
|
|
@ -1,153 +1,17 @@
|
|||
import { useEffect } from 'react';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
import useSWR from 'swr';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import { EmptyState, LinkPrimary, Loading, pageLimit, Pagination, Table } from '@boxyhq/internal-ui';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
||||
import router from 'next/router';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
import { getDisplayTypeFromSinkType } from '@lib/sinkConfigMap';
|
||||
import { TableBodyType } from 'internal-ui/src/shared/Table';
|
||||
import { SecurityLogsConfigs } from 'internal-ui/src/security-logs-config';
|
||||
|
||||
const ConfigList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { paginate, setPaginate, pageTokenMap, setPageTokenMap } = usePaginate();
|
||||
|
||||
let getAppsUrl = `/api/admin/security-logs-config?offset=${paginate.offset}&limit=${pageLimit}`;
|
||||
|
||||
// Use the (next)pageToken mapped to the previous page offset to get the current page
|
||||
if (paginate.offset > 0 && pageTokenMap[paginate.offset - pageLimit]) {
|
||||
getAppsUrl += `&pageToken=${pageTokenMap[paginate.offset - pageLimit]}`;
|
||||
}
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SecurityLogsConfig[]>, ApiError>(getAppsUrl, fetcher);
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
||||
// store the nextPageToken against the pageOffset
|
||||
useEffect(() => {
|
||||
if (nextPageToken) {
|
||||
setPageTokenMap((tokenMap) => ({ ...tokenMap, [paginate.offset]: nextPageToken }));
|
||||
}
|
||||
}, [nextPageToken, paginate.offset]);
|
||||
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const configs = data?.data || [];
|
||||
const noConfigs = configs.length === 0 && paginate.offset === 0;
|
||||
const noMoreResults = configs.length === 0 && paginate.offset > 0;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('bui-shared-name'),
|
||||
wrap: true,
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('bui-shared-type'),
|
||||
wrap: true,
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
key: 'tenant',
|
||||
label: t('bui-shared-tenant'),
|
||||
wrap: true,
|
||||
dataIndex: 'tenant',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: t('bui-shared-actions'),
|
||||
wrap: true,
|
||||
dataIndex: null,
|
||||
},
|
||||
];
|
||||
|
||||
const cols = columns.map(({ label }) => label);
|
||||
|
||||
const body: TableBodyType[] = configs.map((config) => {
|
||||
return {
|
||||
id: config.id,
|
||||
cells: columns.map((column) => {
|
||||
const dataIndex = column.dataIndex as string;
|
||||
|
||||
if (dataIndex === null) {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
text: t('bui-shared-edit'),
|
||||
onClick: () => router.push(`/admin/settings/security-logs/${config.id}/edit`),
|
||||
icon: <PencilIcon className='w-5' />,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (dataIndex === 'type') {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: getDisplayTypeFromSinkType(config.type),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: config[dataIndex],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('security_logs_configs')}</h2>
|
||||
<div className='flex'>
|
||||
<LinkPrimary className='m-2' Icon={PlusIcon} href='/admin/settings/security-logs/new'>
|
||||
{t('new_security_logs_config')}
|
||||
</LinkPrimary>
|
||||
</div>
|
||||
</div>
|
||||
{noConfigs ? (
|
||||
<>
|
||||
<EmptyState title={t('no_security_logs_config')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Table noMoreResults={noMoreResults} cols={cols} body={body} />
|
||||
<Pagination
|
||||
itemsCount={configs.length}
|
||||
offset={paginate.offset}
|
||||
onPrevClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset - pageLimit,
|
||||
});
|
||||
}}
|
||||
onNextClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset + pageLimit,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const urls = {
|
||||
getConfigs: '/api/admin/security-logs-config',
|
||||
createConfig: '/admin/settings/security-logs/new',
|
||||
editLink: (id) => `/admin/settings/security-logs/${id}`,
|
||||
};
|
||||
return <SecurityLogsConfigs urls={urls} />;
|
||||
};
|
||||
|
||||
export default ConfigList;
|
||||
|
|
|
@ -1,132 +1,21 @@
|
|||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { successToast } from '@components/Toaster';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { configMap } from '@lib/sinkConfigMap';
|
||||
import { ButtonPrimary, LinkBack } from '@boxyhq/internal-ui';
|
||||
import { SecurityLogsConfigCreate } from 'internal-ui/src/security-logs-config';
|
||||
|
||||
const NewConfiguration = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState({});
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState(t('select_type'));
|
||||
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch('/api/admin/security-logs-config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name, type: configMap[type].type, config }),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
successToast(t('security_logs_config_new_success'));
|
||||
router.replace(`/admin/settings/security-logs`);
|
||||
}
|
||||
const urls = {
|
||||
createConfig: '/api/admin/security-logs-config',
|
||||
listConfigs: '/admin/settings/security-logs',
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href='/admin/settings/security-logs' />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>{t('security_logs_config_add_new')}</h2>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-type')}</span>
|
||||
</label>
|
||||
<select
|
||||
className='select-bordered select w-full'
|
||||
id='type'
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value);
|
||||
}}
|
||||
required>
|
||||
{[t('select_type'), ...Object.keys(configMap)].map((key) => {
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input'
|
||||
value={name}
|
||||
required={false}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('bui-shared-name')}
|
||||
/>
|
||||
</div>
|
||||
{type && (
|
||||
<>
|
||||
{configMap[type] &&
|
||||
configMap[type].fields.map((field) => {
|
||||
return (
|
||||
<div key={field.index} className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
id={field.name}
|
||||
className='input-bordered input'
|
||||
required
|
||||
onChange={onChange}
|
||||
placeholder={t(field.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<ButtonPrimary loading={loading}>{t('create_config')}</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <SecurityLogsConfigCreate urls={urls} onSuccess={successToast} />;
|
||||
};
|
||||
|
||||
export default NewConfiguration;
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ButtonPrimary, LinkBack } from '@boxyhq/internal-ui';
|
||||
|
||||
import { useRouter } from '../hooks';
|
||||
import { configMap } from './lib';
|
||||
import { Error } from '../shared';
|
||||
|
||||
export const SecurityLogsConfigCreate = ({
|
||||
urls,
|
||||
onSuccess,
|
||||
}: {
|
||||
urls: {
|
||||
createConfig: string;
|
||||
listConfigs: string;
|
||||
};
|
||||
onSuccess: (message: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState({});
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState(t('bui-shared-select-type'));
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(urls.createConfig, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name, type: configMap[type].type, config }),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
return <Error message={response.error.message} />;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-new-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={urls.listConfigs} />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>{t('bui-slc-add')}</h2>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-type')}</span>
|
||||
</label>
|
||||
<select
|
||||
className='select-bordered select w-full'
|
||||
id='type'
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value);
|
||||
}}
|
||||
required>
|
||||
{[t('bui-shared-select-type'), ...Object.keys(configMap)].map((key) => {
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input'
|
||||
value={name}
|
||||
required={false}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('bui-shared-name')}
|
||||
/>
|
||||
</div>
|
||||
{type && (
|
||||
<>
|
||||
{configMap[type] &&
|
||||
configMap[type].fields.map((field) => {
|
||||
return (
|
||||
<div key={field.index} className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
id={field.name}
|
||||
className='input-bordered input'
|
||||
required
|
||||
onChange={onChange}
|
||||
placeholder={t(field.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<ButtonPrimary loading={loading}>{t('bui-slc-create-config')}</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { ButtonDanger, ConfirmationModal } from '../shared';
|
||||
import { useRouter } from '../hooks';
|
||||
import { ApiResponse } from '../types';
|
||||
|
||||
export const SecurityLogsConfigDelete = ({
|
||||
id,
|
||||
urls,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
id: string;
|
||||
urls: { deleteById: (id: string) => string; listConfigs: string };
|
||||
onError: (string) => void;
|
||||
onSuccess: (string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const deleteApp = async () => {
|
||||
const rawResponse = await fetch(urls.deleteById(id), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const response: ApiResponse<unknown> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
onError(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-delete-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='mt-5 flex items-center rounded bg-red-100 p-6 text-red-900'>
|
||||
<div className='flex-1'>
|
||||
<h6 className='mb-1 font-medium'>{t('bui-slc-delete-confirmation')}</h6>
|
||||
<p className='font-light'>{t('bui-slc-logs-noop')}</p>
|
||||
</div>
|
||||
<ButtonDanger
|
||||
type='button'
|
||||
data-modal-toggle='popup-modal'
|
||||
onClick={() => {
|
||||
setDelModalVisible(true);
|
||||
}}>
|
||||
{t('bui-shared-delete')}
|
||||
</ButtonDanger>
|
||||
</section>
|
||||
<ConfirmationModal
|
||||
title={t('bui-slc-delete')}
|
||||
description={t('bui-slc-delete-modal-confirmation')}
|
||||
visible={delModalVisible}
|
||||
onConfirm={deleteApp}
|
||||
onCancel={() => {
|
||||
setDelModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,137 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
|
||||
import { fetcher } from '../utils';
|
||||
import { ButtonPrimary, LinkBack, Loading } from '../shared';
|
||||
import { SinkConfigMapField, getFieldsFromSinkType } from './lib';
|
||||
import { Error } from '../shared';
|
||||
import { ApiError, ApiResponse, ApiSuccess } from '../types';
|
||||
import { useRouter } from '../hooks';
|
||||
import { SecurityLogsConfigDelete } from './SecurityLogsConfigDelete';
|
||||
|
||||
export const SecurityLogsConfigEdit = ({
|
||||
id,
|
||||
urls,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
id: string;
|
||||
urls: {
|
||||
getById: (id: string) => string;
|
||||
updateById: (id: string) => string;
|
||||
deleteById: (id: string) => string;
|
||||
listConfigs: string;
|
||||
};
|
||||
onError: (error: string) => void;
|
||||
onSuccess: (message: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState<any>({});
|
||||
const [fields, setFields] = useState<SinkConfigMapField[]>([]);
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SecurityLogsConfig>, ApiError>(
|
||||
urls.getById(id),
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setConfig(data.data?.config);
|
||||
setFields(getFieldsFromSinkType(data.data?.type) || []);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
<Error message={error.message} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(urls.updateById(id), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config,
|
||||
}),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<SecurityLogsConfig> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
onError(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-update-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={urls.listConfigs} />
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='mt-5 font-bold text-gray-700 md:text-xl'>{t('bui-slc-update')}</h2>
|
||||
</div>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='space-y-3'>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<div className='form-control w-full md:w-1/2' key={field.index}>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
className='input-bordered input'
|
||||
id={field.name}
|
||||
placeholder={t(field.placeholder)}
|
||||
value={config[field.name]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('bui-shared-save-changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<SecurityLogsConfigDelete id={id} urls={urls} onSuccess={onSuccess} onError={onError} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,163 @@
|
|||
import { useEffect } from 'react';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
import useSWR from 'swr';
|
||||
import { EmptyState, LinkPrimary, Loading, pageLimit, Pagination, Table } from '@boxyhq/internal-ui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
||||
|
||||
import { addQueryParamsToPath, fetcher } from '../utils';
|
||||
import { usePaginate, useRouter } from '../hooks';
|
||||
import { TableBodyType } from '../shared/Table';
|
||||
import { ApiError, ApiSuccess } from '../types';
|
||||
import { getDisplayTypeFromSinkType } from './lib';
|
||||
import { Error } from '../shared';
|
||||
|
||||
export const SecurityLogsConfigs = ({
|
||||
urls,
|
||||
}: {
|
||||
urls: {
|
||||
getConfigs: string;
|
||||
createConfig: string;
|
||||
editLink: (id: string) => string;
|
||||
};
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const { paginate, setPaginate, pageTokenMap, setPageTokenMap } = usePaginate(router!);
|
||||
|
||||
const params = {
|
||||
pageOffset: paginate.offset,
|
||||
pageLimit: pageLimit,
|
||||
};
|
||||
|
||||
// For DynamoDB
|
||||
if (paginate.offset > 0 && pageTokenMap[paginate.offset - pageLimit]) {
|
||||
params['pageToken'] = pageTokenMap[paginate.offset - pageLimit];
|
||||
}
|
||||
|
||||
const getConfigsUrl = addQueryParamsToPath(urls.getConfigs, params);
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SecurityLogsConfig[]>, ApiError>(
|
||||
getConfigsUrl,
|
||||
fetcher
|
||||
);
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
||||
// store the nextPageToken against the pageOffset
|
||||
useEffect(() => {
|
||||
if (nextPageToken) {
|
||||
setPageTokenMap((tokenMap) => ({ ...tokenMap, [paginate.offset]: nextPageToken }));
|
||||
}
|
||||
}, [nextPageToken, paginate.offset]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error message={error.message} />;
|
||||
}
|
||||
|
||||
const configs = data?.data || [];
|
||||
const noConfigs = configs.length === 0 && paginate.offset === 0;
|
||||
const noMoreResults = configs.length === 0 && paginate.offset > 0;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('bui-shared-name'),
|
||||
wrap: true,
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('bui-shared-type'),
|
||||
wrap: true,
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
key: 'tenant',
|
||||
label: t('bui-shared-tenant'),
|
||||
wrap: true,
|
||||
dataIndex: 'tenant',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: t('bui-shared-actions'),
|
||||
wrap: true,
|
||||
dataIndex: null,
|
||||
},
|
||||
];
|
||||
|
||||
const cols = columns.map(({ label }) => label);
|
||||
|
||||
const body: TableBodyType[] = configs.map((config) => {
|
||||
return {
|
||||
id: config.id,
|
||||
cells: columns.map((column) => {
|
||||
const dataIndex = column.dataIndex as string;
|
||||
|
||||
if (dataIndex === null) {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
text: t('bui-shared-edit'),
|
||||
onClick: () => router?.replace(urls.editLink(config.id)),
|
||||
icon: <PencilIcon className='w-5' />,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (dataIndex === 'type') {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: getDisplayTypeFromSinkType(config.type),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: config[dataIndex],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('bui-slc')}</h2>
|
||||
<div className='flex'>
|
||||
<LinkPrimary className='m-2' Icon={PlusIcon} href={urls.createConfig}>
|
||||
{t('bui-slc-new')}
|
||||
</LinkPrimary>
|
||||
</div>
|
||||
</div>
|
||||
{noConfigs ? (
|
||||
<>
|
||||
<EmptyState title={t('bui-slc-empty')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Table noMoreResults={noMoreResults} cols={cols} body={body} />
|
||||
<Pagination
|
||||
itemsCount={configs.length}
|
||||
offset={paginate.offset}
|
||||
onPrevClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset - pageLimit,
|
||||
});
|
||||
}}
|
||||
onNextClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset + pageLimit,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export { SecurityLogsConfigs } from './SecurityLogsConfigs';
|
||||
export { SecurityLogsConfigCreate } from './SecurityLogsConfigCreate';
|
||||
export { SecurityLogsConfigEdit } from './SecurityLogsConfigEdit';
|
||||
export { configMap, getDisplayTypeFromSinkType } from './lib';
|
|
@ -19,17 +19,17 @@ export const configMap = {
|
|||
fields: [
|
||||
{
|
||||
index: 1,
|
||||
label: 'splunk_event_collector_url',
|
||||
label: 'bui-splunk-collector-url',
|
||||
name: 'endpoint',
|
||||
type: 'string',
|
||||
placeholder: 'splunk_hec_endpoint_placeholder',
|
||||
placeholder: 'bui-splunk-hec-endpoint-placeholder',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: 'default_token',
|
||||
label: 'bui-default-token',
|
||||
name: 'default_token',
|
||||
type: 'string',
|
||||
placeholder: 'default_token_placeholder',
|
||||
placeholder: 'bui-default-token-placeholder',
|
||||
},
|
||||
],
|
||||
},
|
|
@ -5,6 +5,8 @@ export interface ApiError extends Error {
|
|||
status: number;
|
||||
}
|
||||
|
||||
export type ApiResponse<T = any> = ApiSuccess<T> | { error: ApiError };
|
||||
|
||||
enum DirectorySyncProviders {
|
||||
'azure-scim-v2' = 'Azure SCIM v2.0',
|
||||
'onelogin-scim-v2' = 'OneLogin SCIM v2.0',
|
||||
|
|
|
@ -108,24 +108,6 @@
|
|||
"choose_an_identity_provider_to_continue": "Choose an Identity Provider to continue. If you don't see your Identity Provider, please contact your administrator.",
|
||||
"choose_an_app_to_continue": "Choose an app to continue. If you don't see your app, please contact your administrator.",
|
||||
"no_saml_response_try_again": "No SAMLResponse found. Please try again.",
|
||||
"security_logs_config_new_success": "Security logs config created successfully.",
|
||||
"security_logs_wont_be_sent_to_this": "Security logs won't be sent to this destination.",
|
||||
"confirmation_modal_description_config": "This action cannot be undone. This will permanently delete the Configuration.",
|
||||
"security_logs_configs": "Security Logs Configurations",
|
||||
"no_security_logs_config": "No Security Logs Configuration found.",
|
||||
"splunk_event_collector_url": "Splunk HTTP Event Collector Url",
|
||||
"splunk_hec_endpoint_placeholder": "https://splunk.example.com:8088",
|
||||
"default_token": "Default Token",
|
||||
"default_token_placeholder": "Token generated by splunk for HEC",
|
||||
"security_logs_config_add_new": "Add Security Logs Configuration",
|
||||
"create_config": "Create Configuration",
|
||||
"security_logs_config_success": "Security logs configuration updated successfully.",
|
||||
"security_logs_config_update": "Update Security Logs Configuration",
|
||||
"security_logs_config_delete_success": "Security Logs Configuration deleted successfully",
|
||||
"delete_this_security_logs_config": "Delete this Security Logs Configuration",
|
||||
"delete_the_security_logs_config": "Delete the Security logs config?",
|
||||
"new_security_logs_config": "New Configuration",
|
||||
"select_type": "Select a type",
|
||||
"sso_connection_created_successfully": "SSO Connection created successfully",
|
||||
"sso_connection_deleted_successfully": "SSO Connection deleted successfully",
|
||||
"saml_federation_entity_id_generated": "SP Entity ID generated",
|
||||
|
@ -133,7 +115,10 @@
|
|||
"setup-link-regenerated": "The setup link regenerated.",
|
||||
"setup-link-copied": "The setup link copied to the clipboard.",
|
||||
"setup-link-deleted": "The setup link deleted.",
|
||||
"bui-default-token": "Default Token",
|
||||
"bui-default-token-placeholder": "Token generated by splunk for HEC",
|
||||
"bui-shared-name": "Name",
|
||||
"bui-shared-select-type": "Select a type",
|
||||
"bui-shared-type": "Type",
|
||||
"bui-shared-tenant": "Tenant",
|
||||
"bui-shared-product": "Product",
|
||||
|
@ -163,6 +148,21 @@
|
|||
"bui-shared-favicon-url-desc": "Provide a URL to your favicon. Recommend PNG, SVG, or ICO formats.",
|
||||
"bui-shared-primary-color": "Primary Color",
|
||||
"bui-shared-primary-color-desc": "Primary color will be applied to buttons, links, and other elements.",
|
||||
"bui-slc": "Security Logs Configurations",
|
||||
"bui-slc-add": "Add Security Logs Configuration",
|
||||
"bui-slc-create-config": "Create Configuration",
|
||||
"bui-slc-delete": "Delete the Security logs config?",
|
||||
"bui-slc-delete-confirmation": "Delete this Security Logs Configuration",
|
||||
"bui-slc-delete-modal-confirmation": "This action cannot be undone. This will permanently delete the Configuration.",
|
||||
"bui-slc-delete-success": "Security Logs Configuration deleted successfully",
|
||||
"bui-slc-empty": "No Security Logs Configuration found.",
|
||||
"bui-slc-logs-noop": "Security logs won't be sent to this destination.",
|
||||
"bui-slc-new": "New Configuration",
|
||||
"bui-slc-new-success": "Security logs config created successfully.",
|
||||
"bui-slc-update": "Update Security Logs Configuration",
|
||||
"bui-slc-update-success": "Security logs configuration updated successfully.",
|
||||
"bui-splunk-collector-url": "Splunk HTTP Event Collector Url",
|
||||
"bui-splunk-hec-endpoint-placeholder": "https://splunk.example.com:8088",
|
||||
"bui-wku-heading": "Here are the set of URIs you would need access to:",
|
||||
"bui-wku-idp-configuration-links": "Identity Provider Configuration links",
|
||||
"bui-wku-desc-idp-configuration": "Links for SAML/OIDC IdP setup",
|
||||
|
|
Loading…
Reference in New Issue