Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create categories page #19852

Merged
merged 11 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import styled from 'styled-components';

import { IconButton } from 'components/common';
import { Button, BootstrapModalConfirm } from 'components/bootstrap';
import type { EntityGroupsListResponse } from 'components/entity-groups/Types';
import { useCreateEntityGroup, useUpdateEntityGroup, useDeleteEntityGroup } from 'components/entity-groups/hooks/useEntityGroups';

const DataRow = styled.div`
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;

.default-button {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}

&:hover .default-button {
opacity: 1;
}
`;

const StyledInput = styled.input`
color: #fff;
background-color: #303030;
padding: 6px 12px;
border: 1px solid #525252;

&:focus {
border: 1px solid #5082bc;
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(80 130 188 / 40%);
}
`;

const StyledButton = styled(Button)`
justify-content: flex-end;
`;

const CancelButton = styled(StyledButton)`
margin-right: 5px;
`;

const StyledIconButton = styled(IconButton)`
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
float: right;
font-weight: bold;
line-height: 1;
opacity: 0.5;

&:hover {
background-color: transparent;
opacity: 0.7;
}
`;

type Props = {
entityGroups: EntityGroupsListResponse[];
showAddEntityGroup: boolean;
setShowAddEntityGroup: (boolean) => void;
}

const EntityGroupsList = ({ entityGroups, showAddEntityGroup, setShowAddEntityGroup }: Props) => {
const [editId, setEditId] = React.useState('');
const [editValue, setEditValue] = React.useState('');
const [newEntityGroupTagValue, setNewEntityGroupTagValue] = React.useState('');
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
const [entityGroupToDelete, setEntityGroupToDelete] = React.useState({ id: '', value: '' });

const { createEntityGroup } = useCreateEntityGroup();
const { updateEntityGroup } = useUpdateEntityGroup();
const { deleteEntityGroup } = useDeleteEntityGroup();

const onAddEntityGroup = async () => {
await createEntityGroup({ name: newEntityGroupTagValue });

setNewEntityGroupTagValue('');
setShowAddEntityGroup(false);
};

const resetAddValues = () => {
setNewEntityGroupTagValue('');
setShowAddEntityGroup(false);
};

const handleAddKeyDown = (e: React.KeyboardEvent) => {
if (!newEntityGroupTagValue) return;

if (e.key === 'Enter') {
onAddEntityGroup();
}

if (e.key === 'Escape') {
resetAddValues();
}
};

const onEditEntityGroup = async () => {
await updateEntityGroup({ id: editId, requestObj: { name: editValue } });

setEditId('');
setEditValue('');
};

const resetEditValues = () => {
setEditId('');
setEditValue('');
};

const handleEditKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
onEditEntityGroup();
}

if (e.key === 'Escape') {
resetEditValues();
}
};

const onDeleteEntityGroup = async () => {
await deleteEntityGroup({ entityGroupId: entityGroupToDelete.id });

setShowDeleteModal(false);
setEntityGroupToDelete({ id: '', value: '' });
};

return (
<>
{showAddEntityGroup && (
<DataRow key="new-entity-group">
<div style={{ margin: '0' }}>
<StyledInput id="add-entity-group-input"
data-testid="new-entity-group-input"
type="text"
autoComplete="off"
style={{ marginBottom: '0px', paddingRight: '5px' }}
value={newEntityGroupTagValue}
onChange={(e: React.BaseSyntheticEvent) => setNewEntityGroupTagValue(e.target.value)}
onKeyDown={handleAddKeyDown} />
</div>
<div>
<CancelButton onClick={() => resetAddValues()}>
Cancel
</CancelButton>
<StyledButton bsStyle="success"
data-testid="save-edit-entity-group"
disabled={!newEntityGroupTagValue || false}
onClick={onAddEntityGroup}>
Add
</StyledButton>
</div>
</DataRow>
)}
{entityGroups.map((entityGroup) => {
const isCurrentlyEditing = entityGroup.id === editId;

return (
<DataRow key={entityGroup.id}>
{isCurrentlyEditing ? (
<>
<div style={{ margin: '0' }}>
<StyledInput id="edit-entity-group-input"
data-testid="entity-group-input"
type="text"
autoComplete="off"
style={{ marginBottom: '0px', paddingRight: '5px' }}
value={editValue}
onChange={(e: React.BaseSyntheticEvent) => setEditValue(e.target.value)}
onKeyDown={handleEditKeyDown} />
</div>
<div>
<CancelButton onClick={() => resetEditValues()}>
Cancel
</CancelButton>
<StyledButton bsStyle="success"
data-testid="save-edit-entity-group"
disabled={false}
onClick={onEditEntityGroup}>
Save
</StyledButton>
</div>
</>
) : (
<>
<div style={{ display: 'flex' }}>
<div>{entityGroup.name}</div>
</div>
<div>
<StyledIconButton data-testid="delete-entity-group"
title="Delete entity group"
name="close"
onClick={() => {
setEntityGroupToDelete({ id: entityGroup.id, value: entityGroup.name });
setShowDeleteModal(true);
}} />
<StyledIconButton name="edit"
data-testid="edit-category"
title="Edit category"
onClick={() => {
setEditId(entityGroup.id);
setEditValue(entityGroup.name);
}} />
</div>
</>
)}
<BootstrapModalConfirm showModal={showDeleteModal}
title="Are you sure you want to delete this category?"
onConfirm={() => onDeleteEntityGroup()}
onCancel={() => setShowDeleteModal(false)}>
<div>You are about to delete this tag: {entityGroupToDelete.value}</div>
</BootstrapModalConfirm>
</DataRow>
);
})}
</>
);
};

export default EntityGroupsList;
21 changes: 21 additions & 0 deletions graylog2-web-interface/src/components/entity-groups/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
export type EntityGroupsListResponse = {
entities: any,
name?: string,
id: string,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';

import UserNotification from 'util/UserNotification';
import { qualifyUrl } from 'util/URLUtils';
import fetch from 'logic/rest/FetchProvider';
import type { EntityGroupsListResponse } from 'components/entity-groups/Types';
import type FetchError from 'logic/errors/FetchError';

const fetchEntityGroups = async () => fetch('GET', qualifyUrl('/entity_groups'));

const createEntityGroup = async (requestObj) => {
const requestBody = requestObj;

return fetch('POST', qualifyUrl('/entity_groups'), requestBody);
};

const updateEntityGroup = async ({ id, requestObj }: {id: string, requestObj: { entities?: any, name: string }}) => {
const requestBody = requestObj;

return fetch('PUT', qualifyUrl(`/entity_groups/${id}`), requestBody);
};

const deleteEntityGroup = async ({ entityGroupId }: { entityGroupId: string }) => fetch('DELETE', qualifyUrl(`/entity_groups/${entityGroupId}`));

export const useGetEntityGroups = () => {
const { data, isInitialLoading } = useQuery<EntityGroupsListResponse[], FetchError>(
['get-entity-groups'],
fetchEntityGroups,
{
onError: (errorThrown) => {
UserNotification.error(`Loading entity groups failed with status: ${errorThrown}`,
'Could not load entity groups.');
},
},
);

return ({
data: data,
isInitialLoading,
});
};

export const useCreateEntityGroup = () => {
const queryClient = useQueryClient();

const { mutateAsync, isLoading } = useMutation(
createEntityGroup,
{
onSuccess: () => {
UserNotification.success('New entity group added successfully');
queryClient.invalidateQueries(['get-entity-groups']);
},
onError: (error: Error) => UserNotification.error(error.message),
},
);

return {
createEntityGroup: mutateAsync,
creatingEntityGroup: isLoading,
};
};

export const useUpdateEntityGroup = () => {
const queryClient = useQueryClient();

const { mutateAsync, isLoading } = useMutation(
updateEntityGroup,
{
onSuccess: () => {
UserNotification.success('Entity group updated successfully');
queryClient.invalidateQueries(['get-entity-groups']);
},
onError: (error: Error) => UserNotification.error(error.message),
},
);

return {
updateEntityGroup: mutateAsync,
updatingEntityGroup: isLoading,
};
};

export const useDeleteEntityGroup = () => {
const queryClient = useQueryClient();

const { mutateAsync, isLoading } = useMutation(
deleteEntityGroup,
{
onSuccess: () => {
UserNotification.success('Entity group deleted successfully');
queryClient.invalidateQueries(['get-entity-groups']);
},
onError: (error: Error) => UserNotification.error(error.message),
},
);

return {
deleteEntityGroup: mutateAsync,
deletingEntityGroup: isLoading,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const navigationBindings: PluginExports = {
{ path: Routes.SYSTEM.LOOKUPTABLES.OVERVIEW, description: 'Lookup Tables', permissions: ['lookuptables:read'] },
{ path: Routes.SYSTEM.PIPELINES.OVERVIEW, description: 'Pipelines', permissions: ['pipeline:read', 'pipeline_connection:read'] },
{ path: Routes.SYSTEM.SIDECARS.OVERVIEW, description: 'Sidecars', permissions: ['sidecars:read'] },
{ path: Routes.SYSTEM.ENTITYGROUPS.OVERVIEW, description: 'Entity Groups' },
],
AppConfig.isCloud() && !AppConfig.isFeatureEnabled('cloud_inputs') ? [Routes.SYSTEM.INPUTS] : [],
),
Expand Down
Loading
Loading