diff --git a/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.jsx b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.jsx new file mode 100644 index 0000000000..f2a6356cc1 --- /dev/null +++ b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { noop } from 'lodash'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +import Button from '@common/Button'; +import Modal from '@common/Modal'; +import ListView from '@common/ListView'; + +import { + API_KEY_GENERATION, + CHANGING_SUMA_SETTINGS, + CLEARING_SUMA_SETTINGS, + CLUSTER_CHECKS_EXECUTION_REQUEST, + LOGIN_ATTEMPT, + PROFILE_UPDATE, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + SAVING_SUMA_SETTINGS, + USER_CREATION, + USER_DELETION, + USER_MODIFICATION, +} from '@lib/model/activityLog'; +import classNames from 'classnames'; + +const activityTypesLabels = { + [LOGIN_ATTEMPT]: 'Login Attempt', + [RESOURCE_TAGGING]: 'Tag Added', + [RESOURCE_UNTAGGING]: 'Tag Removed', + [API_KEY_GENERATION]: 'API Key Generated', + [SAVING_SUMA_SETTINGS]: 'SUMA Settings Saved', + [CHANGING_SUMA_SETTINGS]: 'SUMA Settings Changed', + [CLEARING_SUMA_SETTINGS]: 'SUMA Settings Cleared', + [USER_CREATION]: 'User Created', + [USER_MODIFICATION]: 'User Modified', + [USER_DELETION]: 'User Deleted', + [PROFILE_UPDATE]: 'Profile Updated', + [CLUSTER_CHECKS_EXECUTION_REQUEST]: 'Checks Execution Requested', +}; + +const resourceTypesLabels = { + host: 'Host', + cluster: 'Cluster', + database: 'Database', + sap_system: 'SAP System', +}; + +const keys = ['id', 'type', 'resource', 'user', 'message', 'time', 'metadata']; + +const keyToLabel = { + id: 'ID', + type: 'Activity Type', + resource: 'Resource', + user: 'User', + time: 'Created at', + message: 'Message', + metadata: 'Data', +}; + +const toResource = (activityLogEntry) => { + const { metadata, type } = activityLogEntry; + switch (type) { + case LOGIN_ATTEMPT: + return 'Application'; + case RESOURCE_TAGGING: + case RESOURCE_UNTAGGING: + return ( + resourceTypesLabels[metadata.resource_type] ?? + 'Unable to determine resource type' + ); + case USER_CREATION: + case USER_MODIFICATION: + case USER_DELETION: + return 'User'; + case PROFILE_UPDATE: + return 'Profile'; + case SAVING_SUMA_SETTINGS: + case CHANGING_SUMA_SETTINGS: + case CLEARING_SUMA_SETTINGS: + return 'SUMA Settings'; + case API_KEY_GENERATION: + return 'API Key'; + case CLUSTER_CHECKS_EXECUTION_REQUEST: + return 'Cluster Checks'; + default: + return 'Unrecognized resource'; + } +}; + +const renderMetadata = (metadata) => ( + + {`\`\`\`json\n${JSON.stringify(metadata, null, 2)}\n\`\`\``} + +); + +const renderType = (type) => activityTypesLabels[type] ?? type; + +const renderResource = (entry) => ( + {toResource(entry)} +); + +const keyRenderers = { + metadata: renderMetadata, + type: renderType, + resource: renderResource, +}; + +function ActivityLogDetailModal({ open = false, entry, onClose = noop }) { + const data = keys.map((key) => ({ + title: keyToLabel[key] || key, + content: key === 'resource' ? entry : entry[key], + render: keyRenderers[key] || undefined, + className: classNames('col-span-5', { + 'text-gray-500': key !== 'metadata', + }), + })); + + return ( + + +
+ +
+
+ ); +} + +export default ActivityLogDetailModal; diff --git a/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.stories.jsx b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.stories.jsx new file mode 100644 index 0000000000..e235e28b11 --- /dev/null +++ b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.stories.jsx @@ -0,0 +1,58 @@ +import { action } from '@storybook/addon-actions'; +import { + activityLogEntryFactory, + taggingMetadataFactory, +} from '@lib/test-utils/factories/activityLog'; +import { RESOURCE_TAGGING } from '@lib/model/activityLog'; +import { toRenderedEntry } from '@common/ActivityLogOverview/ActivityLogOverview'; +import ActivityLogDetailModal from '.'; + +export default { + title: 'Components/ActivityLogDetailModal', + component: ActivityLogDetailModal, + argTypes: { + open: { + description: 'Whether the dialog is open or not', + control: { + type: 'boolean', + }, + }, + entry: { + description: 'An Avtivity Log entry.', + control: { + type: 'object', + }, + }, + onClose: { + description: 'Callback when the Cancel button is clicked', + control: { type: 'function' }, + }, + }, +}; + +export const Default = { + args: { + open: false, + entry: toRenderedEntry(activityLogEntryFactory.build()), + onClose: action('cancel clicked'), + }, +}; + +export const UnknwonActivityType = { + args: { + ...Default.args, + entry: toRenderedEntry(activityLogEntryFactory.build({ type: 'foo_bar' })), + }, +}; + +export const UnknwonResourceType = { + args: { + ...Default.args, + entry: toRenderedEntry( + activityLogEntryFactory.build({ + type: RESOURCE_TAGGING, + metadata: taggingMetadataFactory.build({ resource_type: 'foo_bar' }), + }) + ), + }, +}; diff --git a/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.test.jsx b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.test.jsx new file mode 100644 index 0000000000..b829f6b80f --- /dev/null +++ b/assets/js/common/ActivityLogDetailsModal/ActivityLogDetailModal.test.jsx @@ -0,0 +1,204 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import { + activityLogEntryFactory, + taggingMetadataFactory, + untaggingMetadataFactory, +} from '@lib/test-utils/factories/activityLog'; + +import { + LOGIN_ATTEMPT, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + API_KEY_GENERATION, + CHANGING_SUMA_SETTINGS, + CLEARING_SUMA_SETTINGS, + SAVING_SUMA_SETTINGS, + CLUSTER_CHECKS_EXECUTION_REQUEST, + PROFILE_UPDATE, + USER_CREATION, + USER_DELETION, + USER_MODIFICATION, + LEVEL_WARNING, +} from '@lib/model/activityLog'; +import { toRenderedEntry } from '@common/ActivityLogOverview/ActivityLogOverview'; +import ActivityLogDetailModal from '.'; + +describe('ActivityLogDetailModal component', () => { + const scenarios = [ + { + name: LOGIN_ATTEMPT, + entry: activityLogEntryFactory.build({ + type: LOGIN_ATTEMPT, + metadata: {}, + }), + expectedActivityType: 'Login Attempt', + expectedResource: 'Application', + }, + { + name: RESOURCE_TAGGING, + entry: activityLogEntryFactory.build({ + type: RESOURCE_TAGGING, + metadata: taggingMetadataFactory.build({ + resource_type: 'host', + }), + }), + expectedActivityType: 'Tag Added', + expectedResource: 'Host', + }, + { + name: RESOURCE_UNTAGGING, + entry: activityLogEntryFactory.build({ + type: RESOURCE_UNTAGGING, + level: LEVEL_WARNING, + metadata: untaggingMetadataFactory.build({ + resource_type: 'cluster', + }), + }), + expectedActivityType: 'Tag Removed', + expectedResource: 'Cluster', + }, + { + name: API_KEY_GENERATION, + entry: activityLogEntryFactory.build({ + type: API_KEY_GENERATION, + }), + expectedActivityType: 'API Key Generated', + expectedResource: 'API Key', + }, + { + name: SAVING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + type: SAVING_SUMA_SETTINGS, + }), + expectedActivityType: 'SUMA Settings Saved', + expectedResource: 'SUMA Settings', + }, + { + name: CHANGING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + type: CHANGING_SUMA_SETTINGS, + }), + expectedActivityType: 'SUMA Settings Changed', + expectedResource: 'SUMA Settings', + }, + { + name: CLEARING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + type: CLEARING_SUMA_SETTINGS, + }), + expectedActivityType: 'SUMA Settings Cleared', + expectedResource: 'SUMA Settings', + }, + { + name: USER_CREATION, + entry: activityLogEntryFactory.build({ + type: USER_CREATION, + }), + expectedActivityType: 'User Created', + expectedResource: 'User', + userRelatedActivity: true, + }, + { + name: USER_MODIFICATION, + entry: activityLogEntryFactory.build({ + type: USER_MODIFICATION, + }), + expectedActivityType: 'User Modified', + expectedResource: 'User', + userRelatedActivity: true, + }, + { + name: USER_DELETION, + entry: activityLogEntryFactory.build({ + type: USER_DELETION, + }), + expectedActivityType: 'User Deleted', + expectedResource: 'User', + userRelatedActivity: true, + }, + { + name: PROFILE_UPDATE, + entry: activityLogEntryFactory.build({ + type: PROFILE_UPDATE, + }), + expectedActivityType: 'Profile Updated', + expectedResource: 'Profile', + }, + { + name: CLUSTER_CHECKS_EXECUTION_REQUEST, + entry: activityLogEntryFactory.build({ + type: CLUSTER_CHECKS_EXECUTION_REQUEST, + }), + expectedActivityType: 'Checks Execution Requested', + expectedResource: 'Cluster Checks', + }, + ]; + + it.each(scenarios)( + 'should render detail for activity entry `$name`', + async ({ + entry, + expectedActivityType, + expectedResource, + userRelatedActivity = false, + }) => { + const { id } = entry; + await act(async () => { + render(); + }); + + expect(screen.getByText('ID')).toBeVisible(); + expect(screen.getByText(id)).toBeVisible(); + + expect(screen.getByText('Activity Type')).toBeVisible(); + expect(screen.getByText(expectedActivityType)).toBeVisible(); + + expect(screen.getByText('Resource')).toBeVisible(); + const resource = screen.getByLabelText('activity-log-resource'); + expect(resource).toBeVisible(); + expect(resource).toHaveTextContent(expectedResource); + + const userReferences = screen.getAllByText('User'); + if (userRelatedActivity) { + expect(userReferences).toHaveLength(2); + } + userReferences.forEach((userReference) => { + expect(userReference).toBeVisible(); + }); + + expect(screen.getByText('Message')).toBeVisible(); + expect(screen.getByText('Created at')).toBeVisible(); + } + ); + + it('should render detail for unknown activity type', async () => { + const entry = activityLogEntryFactory.build({ type: 'foo_bar' }); + await act(async () => { + render(); + }); + + expect(screen.getByText('foo_bar')).toBeVisible(); + expect(screen.getByText('Unrecognized resource')).toBeVisible(); + expect(screen.getByText('Unrecognized activity')).toBeVisible(); + }); + + it('should call onClose when the close button is clicked', async () => { + const onClose = jest.fn(); + await act(async () => { + render( + + ); + }); + + await userEvent.click(screen.getByText('Close')); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/assets/js/common/ActivityLogDetailsModal/index.js b/assets/js/common/ActivityLogDetailsModal/index.js new file mode 100644 index 0000000000..a2c54267ef --- /dev/null +++ b/assets/js/common/ActivityLogDetailsModal/index.js @@ -0,0 +1,3 @@ +import ActivityLogDetailModal from './ActivityLogDetailModal'; + +export default ActivityLogDetailModal; diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx new file mode 100644 index 0000000000..3b3e612479 --- /dev/null +++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { noop } from 'lodash'; +import { + EOS_BUG_REPORT_OUTLINED, + EOS_ERROR_OUTLINED, + EOS_INFO_OUTLINED, + EOS_KEYBOARD_ARROW_RIGHT_FILLED, + EOS_WARNING_OUTLINED, +} from 'eos-icons-react'; +import Table from '@common/Table'; +import Tooltip from '@common/Tooltip'; + +import { + API_KEY_GENERATION, + CHANGING_SUMA_SETTINGS, + CLEARING_SUMA_SETTINGS, + CLUSTER_CHECKS_EXECUTION_REQUEST, + LOGIN_ATTEMPT, + PROFILE_UPDATE, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + SAVING_SUMA_SETTINGS, + USER_CREATION, + USER_DELETION, + USER_MODIFICATION, + ACTIVITY_LOG_LEVELS, + LEVEL_DEBUG, + LEVEL_ERROR, + LEVEL_INFO, + LEVEL_WARNING, +} from '@lib/model/activityLog'; + +import ActivityLogDetailModal from '@common/ActivityLogDetailsModal'; +import { format } from 'date-fns'; + +const toMessage = (activityLogEntry) => { + const { metadata, type } = activityLogEntry; + + switch (type) { + case LOGIN_ATTEMPT: + return metadata?.reason ? 'Login failed' : 'User logged in'; + case RESOURCE_TAGGING: + return `Tag "${metadata.added_tag}" added to "${metadata.resource_id}"`; + case RESOURCE_UNTAGGING: + return `Tag "${metadata.removed_tag}" removed from "${metadata.resource_id}"`; + case USER_CREATION: + return 'User was created'; + case USER_MODIFICATION: + return 'User was modified'; + case USER_DELETION: + return 'User was deleted'; + case PROFILE_UPDATE: + return 'User modified profile'; + case SAVING_SUMA_SETTINGS: + return 'SUMA Settings was saved'; + case CHANGING_SUMA_SETTINGS: + return 'SUMA Settings was changed'; + case CLEARING_SUMA_SETTINGS: + return 'SUMA Settings was cleared'; + case API_KEY_GENERATION: + return 'API Key was generated'; + case CLUSTER_CHECKS_EXECUTION_REQUEST: + return 'Checks execution requested for cluster'; + default: + return 'Unrecognized activity'; + } +}; + +const logLevelToIcon = { + [LEVEL_DEBUG]: , + [LEVEL_INFO]: , + [LEVEL_WARNING]: , + [LEVEL_ERROR]: , +}; +const logLevelToLabel = { + [LEVEL_DEBUG]: 'Debug', + [LEVEL_INFO]: 'Info', + [LEVEL_WARNING]: 'Warning', + [LEVEL_ERROR]: 'Error', +}; + +export const toRenderedEntry = (entry) => ({ + id: entry.id, + type: entry.type, + time: format(entry.occurred_on, 'yyyy-MM-dd HH:mm:ss'), + message: toMessage(entry), + user: entry.actor, + level: ACTIVITY_LOG_LEVELS.includes(entry?.level) ? entry?.level : LEVEL_INFO, + metadata: entry.metadata, +}); + +function ActivityLogOverview({ + activityLog, + activityLogDetailModalOpen = false, + onActivityLogEntryClick = noop, + onCloseActivityLogEntryDetails = noop, +}) { + const [selectedEntry, setEntry] = useState({}); + + const activityLogTableConfig = { + pagination: true, + usePadding: false, + columns: [ + { + title: 'Time', + key: 'time', + }, + { + title: 'Message', + key: 'message', + }, + { + title: 'User', + key: 'user', + }, + { + title: 'Level', + key: 'level', + className: 'text-center', + render: (level) => ( + + + {logLevelToIcon[level]} + + + ), + }, + { + title: '', + key: 'metadata', + render: (_metadata, logEntry) => ( +
{ + setEntry(logEntry); + onActivityLogEntryClick(); + }} + onKeyDown={() => {}} + > + +
+ ), + }, + ], + }; + + return ( + <> + + + + ); +} + +export default ActivityLogOverview; diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx new file mode 100644 index 0000000000..19e0e069ae --- /dev/null +++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx @@ -0,0 +1,49 @@ +import { activityLogEntryFactory } from '@lib/test-utils/factories/activityLog'; +import _ from 'lodash'; +import ActivityLogOverview from './ActivityLogOverview'; + +export default { + title: 'Components/ActivityLogOverview', + component: ActivityLogOverview, + argTypes: { + activityLog: { + description: 'List of the activity log entries', + control: { + type: 'array', + }, + }, + }, +}; + +export const Default = { + args: { + activityLog: activityLogEntryFactory.buildList(20), + }, +}; + +export const Empty = { + args: { + activityLog: [], + }, +}; + +export const UnknwonActivityType = { + args: { + ...Default.args, + activityLog: [activityLogEntryFactory.build({ type: 'foo_bar' })], + }, +}; + +export const UnknwonLevel = { + args: { + ...Default.args, + activityLog: [activityLogEntryFactory.build({ level: 'foo_bar' })], + }, +}; + +export const MissingLevel = { + args: { + ...Default.args, + activityLog: [_.omit(activityLogEntryFactory.build(), 'level')], + }, +}; diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx new file mode 100644 index 0000000000..2bcb91c591 --- /dev/null +++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { faker } from '@faker-js/faker'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + activityLogEntryFactory, + taggingMetadataFactory, + untaggingMetadataFactory, +} from '@lib/test-utils/factories/activityLog'; +import { + LOGIN_ATTEMPT, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + API_KEY_GENERATION, + CHANGING_SUMA_SETTINGS, + CLEARING_SUMA_SETTINGS, + SAVING_SUMA_SETTINGS, + CLUSTER_CHECKS_EXECUTION_REQUEST, + PROFILE_UPDATE, + USER_CREATION, + USER_DELETION, + USER_MODIFICATION, + LEVEL_DEBUG, + LEVEL_ERROR, + LEVEL_INFO, + LEVEL_WARNING, +} from '@lib/model/activityLog'; +import '@testing-library/jest-dom'; + +import ActivityLogOverview from '.'; + +describe('Activity Log Overview', () => { + it('should render an empty activity log', () => { + render(); + + expect(screen.getByText('No data available')).toBeVisible(); + }); + + const scenarios = [ + { + name: LOGIN_ATTEMPT, + entry: activityLogEntryFactory.build({ + actor: 'admin', + type: LOGIN_ATTEMPT, + level: LEVEL_DEBUG, + metadata: {}, + }), + expectedUser: 'admin', + expectedMessage: 'User logged in', + expectedLevel: 'debug', + }, + { + name: RESOURCE_TAGGING, + entry: activityLogEntryFactory.build({ + actor: 'foo', + type: RESOURCE_TAGGING, + level: LEVEL_INFO, + metadata: taggingMetadataFactory.build({ + resource_type: 'host', + added_tag: 'bar', + resource_id: 'foo-bar', + }), + }), + expectedUser: 'foo', + expectedMessage: 'Tag "bar" added to "foo-bar"', + expectedLevel: 'info', + }, + { + name: RESOURCE_UNTAGGING, + entry: activityLogEntryFactory.build({ + actor: 'bar', + type: RESOURCE_UNTAGGING, + level: LEVEL_WARNING, + metadata: untaggingMetadataFactory.build({ + resource_type: 'cluster', + removed_tag: 'foo', + resource_id: 'bar-foo', + }), + }), + expectedUser: 'bar', + expectedMessage: 'Tag "foo" removed from "bar-foo"', + expectedLevel: 'warning', + }, + { + name: API_KEY_GENERATION, + entry: activityLogEntryFactory.build({ + actor: 'baz', + type: API_KEY_GENERATION, + level: LEVEL_ERROR, + }), + expectedUser: 'baz', + expectedMessage: 'API Key was generated', + expectedLevel: 'error', + }, + { + name: SAVING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + actor: 'user-1', + type: SAVING_SUMA_SETTINGS, + }), + expectedUser: 'user-1', + expectedMessage: 'SUMA Settings was saved', + }, + { + name: CHANGING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + actor: 'user-2', + type: CHANGING_SUMA_SETTINGS, + }), + expectedUser: 'user-2', + expectedMessage: 'SUMA Settings was changed', + }, + { + name: CLEARING_SUMA_SETTINGS, + entry: activityLogEntryFactory.build({ + actor: 'user-3', + type: CLEARING_SUMA_SETTINGS, + }), + expectedUser: 'user-3', + expectedMessage: 'SUMA Settings was cleared', + }, + { + name: USER_CREATION, + entry: activityLogEntryFactory.build({ + actor: 'user-4', + type: USER_CREATION, + }), + expectedUser: 'user-4', + expectedMessage: 'User was created', + }, + { + name: USER_MODIFICATION, + entry: activityLogEntryFactory.build({ + actor: 'user-5', + type: USER_MODIFICATION, + }), + expectedUser: 'user-5', + expectedMessage: 'User was modified', + }, + { + name: USER_DELETION, + entry: activityLogEntryFactory.build({ + actor: 'user-6', + type: USER_DELETION, + }), + expectedUser: 'user-6', + expectedMessage: 'User was deleted', + }, + { + name: PROFILE_UPDATE, + entry: activityLogEntryFactory.build({ + actor: 'user-7', + type: PROFILE_UPDATE, + }), + expectedUser: 'user-7', + expectedMessage: 'User modified profile', + }, + { + name: CLUSTER_CHECKS_EXECUTION_REQUEST, + entry: activityLogEntryFactory.build({ + actor: 'user-8', + type: CLUSTER_CHECKS_EXECUTION_REQUEST, + }), + expectedUser: 'user-8', + expectedMessage: 'Checks execution requested for cluster', + }, + ]; + + it.each(scenarios)( + 'should render log entry for activity `$name`', + ({ entry, expectedUser, expectedMessage, expectedLevel }) => { + render(); + + expect(screen.getByText(expectedMessage)).toBeVisible(); + expect(screen.getByText(expectedUser)).toBeVisible(); + if (expectedLevel) { + expect( + screen.getByLabelText(`log-level-${expectedLevel}`) + ).toBeVisible(); + } + } + ); + + it('should call onActivityLogEntryClick when clicking on an entry in the table', async () => { + const onActivityLogEntryClick = jest.fn(); + const id = faker.string.uuid(); + render( + + ); + + await userEvent.click(screen.getByLabelText(`entry-${id}`)); + expect(onActivityLogEntryClick).toHaveBeenCalled(); + }); +}); diff --git a/assets/js/common/ActivityLogOverview/index.js b/assets/js/common/ActivityLogOverview/index.js new file mode 100644 index 0000000000..9c66b43746 --- /dev/null +++ b/assets/js/common/ActivityLogOverview/index.js @@ -0,0 +1,3 @@ +import ActivityLogOverview from './ActivityLogOverview'; + +export default ActivityLogOverview; diff --git a/assets/js/lib/model/activityLog.js b/assets/js/lib/model/activityLog.js new file mode 100644 index 0000000000..0d840eef63 --- /dev/null +++ b/assets/js/lib/model/activityLog.js @@ -0,0 +1,40 @@ +export const LOGIN_ATTEMPT = 'login_attempt'; +export const RESOURCE_TAGGING = 'resource_tagging'; +export const RESOURCE_UNTAGGING = 'resource_untagging'; +export const API_KEY_GENERATION = 'api_key_generation'; +export const SAVING_SUMA_SETTINGS = 'saving_suma_settings'; +export const CHANGING_SUMA_SETTINGS = 'changing_suma_settings'; +export const CLEARING_SUMA_SETTINGS = 'clearing_suma_settings'; +export const USER_CREATION = 'user_creation'; +export const USER_MODIFICATION = 'user_modification'; +export const USER_DELETION = 'user_deletion'; +export const PROFILE_UPDATE = 'profile_update'; +export const CLUSTER_CHECKS_EXECUTION_REQUEST = + 'cluster_checks_execution_request'; + +export const ACTIVITY_TYPES = [ + LOGIN_ATTEMPT, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + API_KEY_GENERATION, + SAVING_SUMA_SETTINGS, + CHANGING_SUMA_SETTINGS, + CLEARING_SUMA_SETTINGS, + USER_CREATION, + USER_MODIFICATION, + USER_DELETION, + PROFILE_UPDATE, + CLUSTER_CHECKS_EXECUTION_REQUEST, +]; + +export const LEVEL_DEBUG = 'debug'; +export const LEVEL_INFO = 'info'; +export const LEVEL_WARNING = 'warning'; +export const LEVEL_ERROR = 'error'; + +export const ACTIVITY_LOG_LEVELS = [ + LEVEL_DEBUG, + LEVEL_INFO, + LEVEL_WARNING, + LEVEL_ERROR, +]; diff --git a/assets/js/lib/test-utils/factories/activityLog.js b/assets/js/lib/test-utils/factories/activityLog.js new file mode 100644 index 0000000000..3fb88a8f9e --- /dev/null +++ b/assets/js/lib/test-utils/factories/activityLog.js @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker'; +import { Factory } from 'fishery'; +import { + ACTIVITY_TYPES, + RESOURCE_TAGGING, + RESOURCE_UNTAGGING, + ACTIVITY_LOG_LEVELS, +} from '@lib/model/activityLog'; +import { randomObjectFactory } from '.'; + +const taggableResourceTypes = ['host', 'cluster', 'database', 'sap_system']; + +export const taggingMetadataFactory = Factory.define(() => ({ + resource_id: faker.string.uuid(), + resource_type: faker.helpers.arrayElement(taggableResourceTypes), + added_tag: faker.lorem.word(), +})); + +export const untaggingMetadataFactory = Factory.define(() => ({ + resource_id: faker.string.uuid(), + resource_type: faker.helpers.arrayElement(taggableResourceTypes), + removed_tag: faker.lorem.word(), +})); + +const metadataForActivity = (activityType) => { + switch (activityType) { + case RESOURCE_TAGGING: + return taggingMetadataFactory.build(); + case RESOURCE_UNTAGGING: + return untaggingMetadataFactory.build(); + default: + return randomObjectFactory.build(); + } +}; + +export const activityLogEntryFactory = Factory.define(() => { + const activityType = faker.helpers.arrayElement(ACTIVITY_TYPES); + + return { + id: faker.string.uuid(), + actor: faker.internet.userName(), + type: activityType, + occurred_on: faker.date.anytime(), + metadata: metadataForActivity(activityType), + level: faker.helpers.arrayElement(ACTIVITY_LOG_LEVELS), + }; +});