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

Added data streams privileges to better control delete actions in UI #83573

Merged
merged 9 commits into from
Nov 26, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type TestSubjects =
| 'createTemplateButton'
| 'dataStreamsEmptyPromptTemplateLink'
| 'dataStreamTable'
| 'deleteDataStreamsButton'
| 'deleteDataStreamButton'
| 'deleteSystemTemplateCallOut'
| 'deleteTemplateButton'
| 'deleteTemplatesConfirmation'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface DataStreamsTabTestBed extends TestBed<TestSubjects> {
clickNameAt: (index: number) => void;
clickIndicesAt: (index: number) => void;
clickDeleteActionAt: (index: number) => void;
selectDataStream: (name: string, selected: boolean) => void;
clickConfirmDelete: () => void;
clickDeleteDataStreamButton: () => void;
clickDetailPanelIndexTemplateLink: () => void;
Expand Down Expand Up @@ -125,6 +126,13 @@ export const setup = async (overridingDependencies: any = {}): Promise<DataStrea
findDeleteActionAt(index).simulate('click');
};

const selectDataStream = (name: string, selected: boolean) => {
const {
form: { selectCheckBox },
} = testBed;
selectCheckBox(`checkboxSelectRow-${name}`, selected);
};

const findDeleteConfirmationModal = () => {
const { find } = testBed;
return find('deleteDataStreamsConfirmation');
Expand Down Expand Up @@ -194,6 +202,7 @@ export const setup = async (overridingDependencies: any = {}): Promise<DataStrea
clickNameAt,
clickIndicesAt,
clickDeleteActionAt,
selectDataStream,
clickConfirmDelete,
clickDeleteDataStreamButton,
clickDetailPanelIndexTemplateLink,
Expand Down Expand Up @@ -223,6 +232,9 @@ export const createDataStreamPayload = (dataStream: Partial<DataStream>): DataSt
indexTemplateName: 'indexTemplate',
storageSize: '1b',
maxTimeStamp: 420,
privileges: {
delete_index: true,
},
...dataStream,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,77 @@ describe('Data Streams tab', () => {
expect(tableCellsValues).toEqual([['', 'non-managed-data-stream', 'green', '1', 'Delete']]);
});
});

describe('data stream privileges', () => {
describe('delete', () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;

const dataStreamWithDelete = createDataStreamPayload({
name: 'dataStreamWithDelete',
privileges: { delete_index: true },
});
const dataStreamNoDelete = createDataStreamPayload({
name: 'dataStreamNoDelete',
privileges: { delete_index: false },
});

beforeEach(async () => {
setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]);

testBed = await setup({ history: createMemoryHistory() });
await act(async () => {
testBed.actions.goToDataStreamsList();
});
testBed.component.update();
});

test('displays/hides delete button depending on data streams privileges', async () => {
const { table } = testBed;
const { tableCellsValues } = table.getMetaData('dataStreamTable');

expect(tableCellsValues).toEqual([
['', 'dataStreamNoDelete', 'green', '1', ''],
['', 'dataStreamWithDelete', 'green', '1', 'Delete'],
]);
});

test('displays/hides delete action depending on data streams privileges', async () => {
const {
actions: { selectDataStream },
find,
} = testBed;

selectDataStream('dataStreamNoDelete', true);
expect(find('deleteDataStreamsButton').exists()).toBeFalsy();

selectDataStream('dataStreamWithDelete', true);
expect(find('deleteDataStreamsButton').exists()).toBeFalsy();

selectDataStream('dataStreamNoDelete', false);
expect(find('deleteDataStreamsButton').exists()).toBeTruthy();
});

test('displays delete button in detail panel', async () => {
const {
actions: { clickNameAt },
find,
} = testBed;
setLoadDataStreamResponse(dataStreamWithDelete);
await clickNameAt(1);

expect(find('deleteDataStreamButton').exists()).toBeTruthy();
});

test('hides delete button in detail panel', async () => {
const {
actions: { clickNameAt },
find,
} = testBed;
setLoadDataStreamResponse(dataStreamNoDelete);
await clickNameAt(0);

expect(find('deleteDataStreamButton').exists()).toBeFalsy();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataS
store_size: storageSize,
maximum_timestamp: maxTimeStamp,
_meta,
privileges,
} = dataStreamFromEs;

return {
Expand All @@ -37,6 +38,7 @@ export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataS
storageSize,
maxTimeStamp,
_meta,
privileges,
};
}

Expand Down
16 changes: 12 additions & 4 deletions x-pack/plugins/index_management/common/types/data_streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ interface TimestampFieldFromEs {

type TimestampField = TimestampFieldFromEs;

interface MetaFieldFromEs {
interface MetaFromEs {
managed_by: string;
package: any;
managed: boolean;
}

type MetaField = MetaFieldFromEs;
type Meta = MetaFromEs;

interface PrivilegesFromEs {
delete_index: boolean;
}

type Privileges = PrivilegesFromEs;

export type HealthFromEs = 'GREEN' | 'YELLOW' | 'RED';

Expand All @@ -25,12 +31,13 @@ export interface DataStreamFromEs {
timestamp_field: TimestampFieldFromEs;
indices: DataStreamIndexFromEs[];
generation: number;
_meta?: MetaFieldFromEs;
_meta?: MetaFromEs;
status: HealthFromEs;
template: string;
ilm_policy?: string;
store_size?: string;
maximum_timestamp?: number;
privileges: PrivilegesFromEs;
}

export interface DataStreamIndexFromEs {
Expand All @@ -50,7 +57,8 @@ export interface DataStream {
ilmPolicyName?: string;
storageSize?: string;
maxTimeStamp?: number;
_meta?: MetaField;
_meta?: Meta;
privileges: Privileges;
}

export interface DataStreamIndex {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
</EuiButtonEmpty>
</EuiFlexItem>

{!isLoading && !error ? (
{!isLoading && !error && dataStream?.privileges.delete_index ? (
<EuiFlexItem grow={false}>
<EuiButton
color="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
history,
}) => {
const { isDeepLink } = extractQueryParams(search);
const decodedDataStreamName = attemptToURIDecode(dataStreamName);

const {
core: { getUrlForApp },
Expand Down Expand Up @@ -241,8 +242,8 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa

<DataStreamTable
filters={
isDeepLink && dataStreamName !== undefined
? `name="${attemptToURIDecode(dataStreamName)}"`
isDeepLink && decodedDataStreamName !== undefined
? `name="${decodedDataStreamName}"`
: ''
}
dataStreams={filteredDataStreams}
Expand All @@ -262,9 +263,9 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
If the user has been deep-linked, they'll expect to see the detail panel because it reflects
the URL state, even if there are no data streams or if there was an error loading them.
*/}
{dataStreamName && (
{decodedDataStreamName && (
<DataStreamDetailPanel
dataStreamName={attemptToURIDecode(dataStreamName)!}
dataStreamName={decodedDataStreamName}
onClose={(shouldReload?: boolean) => {
history.push(`/${Section.DataStreams}`);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
},
isPrimary: true,
'data-test-subj': 'deleteDataStream',
available: ({ privileges: { delete_index: deleteIndex } }: DataStream) => deleteIndex,
},
],
});
Expand All @@ -188,9 +189,10 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
incremental: true,
},
toolsLeft:
selection.length > 0 ? (
selection.length > 0 &&
selection.every((dataStream: DataStream) => dataStream.privileges.delete_index) ? (
<EuiButton
data-test-subj="deletDataStreamsButton"
data-test-subj="deleteDataStreamsButton"
onClick={() => setDataStreamsToDelete(selection.map(({ name }: DataStream) => name))}
color="danger"
>
Expand Down
22 changes: 0 additions & 22 deletions x-pack/plugins/index_management/server/client/elasticsearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,6 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
const dataManagement = Client.prototype.dataManagement.prototype;

// Data streams
dataManagement.getDataStreams = ca({
urls: [
{
fmt: '/_data_stream',
},
],
method: 'GET',
});

dataManagement.getDataStream = ca({
urls: [
{
fmt: '/_data_stream/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'GET',
});

// We don't allow the user to create a data stream in the UI or API. We're just adding this here
// to enable the API integration tests.
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/index_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { PLUGIN } from '../common';
import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { License, IndexDataEnricher } from './services';
import { isEsError } from './shared_imports';
import { isEsError, handleEsError } from './shared_imports';
import { elasticsearchJsPlugin } from './client/elasticsearch';

export interface DataManagementContext {
Expand Down Expand Up @@ -110,6 +110,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
indexDataEnricher: this.indexDataEnricher,
lib: {
isEsError,
handleEsError,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('GET privileges', () => {
indexDataEnricher: mockedIndexDataEnricher,
lib: {
isEsError: jest.fn(),
handleEsError: jest.fn(),
},
});

Expand Down Expand Up @@ -123,6 +124,7 @@ describe('GET privileges', () => {
indexDataEnricher: mockedIndexDataEnricher,
lib: {
isEsError: jest.fn(),
handleEsError: jest.fn(),
},
});

Expand Down
Loading