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

feat: support external models in deployed model list #248

Merged
merged 11 commits into from
Aug 29, 2023
24 changes: 24 additions & 0 deletions public/apis/__mocks__/connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export class Connector {
public async getAll() {
return {
data: [
{
id: 'external-connector-1-id',
name: 'External Connector 1',
},
],
total_connectors: 1,
};
}

public async getAllInternal() {
return {
data: ['Internal Connector 1', 'Common Connector'],
};
}
}
11 changes: 11 additions & 0 deletions public/apis/api_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Connector } from './connector';
import { Model } from './model';
import { Profile } from './profile';

const apiInstanceStore: {
model: Model | undefined;
profile: Profile | undefined;
connector: Connector | undefined;
} = {
model: undefined,
profile: undefined,
connector: undefined,
};

export class APIProvider {
public static getAPI(type: 'model'): Model;
public static getAPI(type: 'profile'): Profile;
public static getAPI(type: 'connector'): Connector;
public static getAPI(type: keyof typeof apiInstanceStore) {
if (apiInstanceStore[type]) {
return apiInstanceStore[type]!;
Expand All @@ -32,9 +36,16 @@ export class APIProvider {
apiInstanceStore.profile = newInstance;
return newInstance;
}
case 'connector': {
const newInstance = new Connector();
apiInstanceStore.connector = newInstance;
return newInstance;
}
}
}
public static clear() {
apiInstanceStore.model = undefined;
apiInstanceStore.profile = undefined;
apiInstanceStore.connector = undefined;
}
}
35 changes: 35 additions & 0 deletions public/apis/connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
CONNECTOR_API_ENDPOINT,
INTERNAL_CONNECTOR_API_ENDPOINT,
} from '../../server/routes/constants';
import { InnerHttpProvider } from './inner_http_provider';

export interface GetAllConnectorResponse {
data: Array<{
id: string;
name: string;
description?: string;
}>;
total_connectors: number;
}

interface GetAllInternalConnectorResponse {
data: string[];
}

export class Connector {
public getAll() {
return InnerHttpProvider.getHttp().get<GetAllConnectorResponse>(CONNECTOR_API_ENDPOINT);
}

public getAllInternal() {
return InnerHttpProvider.getHttp().get<GetAllInternalConnectorResponse>(
INTERNAL_CONNECTOR_API_ENDPOINT
);
}
}
9 changes: 8 additions & 1 deletion public/apis/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export interface ModelSearchItem {
current_worker_node_count: number;
planning_worker_node_count: number;
planning_worker_nodes: string[];
connector_id?: string;
connector?: {
name: string;
description?: string;
};
}

export interface ModelSearchResponse {
Expand All @@ -30,9 +35,11 @@ export class Model {
size: number;
states?: MODEL_STATE[];
nameOrId?: string;
extraQuery?: Record<string, any>;
}) {
const { extraQuery, ...restQuery } = query;
return InnerHttpProvider.getHttp().get<ModelSearchResponse>(MODEL_API_ENDPOINT, {
query,
query: extraQuery ? { ...restQuery, extra_query: JSON.stringify(extraQuery) } : restQuery,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import userEvent from '@testing-library/user-event';

import { OptionsFilter } from '../options_filter';
import { render, screen } from '../../../../../test/test_utils';

describe('<OptionsFilter />', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('should render "Tags" as filter name by default', () => {
render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={[]}
value={[]}
onChange={() => {}}
/>
);
expect(screen.getByText('Tags')).toBeInTheDocument();
});

it('should render Tags with 2 active filter', () => {
render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={[]}
value={['foo', 'bar']}
onChange={() => {}}
/>
);
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
});

it('should render options filter after filter button clicked', async () => {
render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={['foo', 'bar']}
onChange={() => {}}
/>
);
expect(screen.queryByText('foo')).not.toBeInTheDocument();
expect(screen.queryByPlaceholderText('Search Tags')).not.toBeInTheDocument();

await userEvent.click(screen.getByText('Tags'));

expect(screen.getByText('foo')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search Tags')).toBeInTheDocument();
});

it('should render passed footer after filter button clicked', async () => {
const { getByText, queryByText } = render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={['foo', 'bar']}
onChange={() => {}}
footer="footer"
/>
);
expect(queryByText('footer')).not.toBeInTheDocument();

await userEvent.click(screen.getByText('Tags'));
expect(getByText('footer')).toBeInTheDocument();
});

it('should only show "bar" after search', async () => {
render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={['foo', 'bar']}
onChange={() => {}}
/>
);

await userEvent.click(screen.getByText('Tags'));
expect(screen.getByText('foo')).toBeInTheDocument();

await userEvent.type(screen.getByPlaceholderText('Search Tags'), 'bAr{enter}');
expect(screen.queryByText('foo')).not.toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
});

it('should call onChange with consistent value after option click', async () => {
const onChangeMock = jest.fn();
const { rerender } = render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={[]}
onChange={onChangeMock}
/>
);

expect(onChangeMock).not.toHaveBeenCalled();

await userEvent.click(screen.getByText('Tags'));
await userEvent.click(screen.getByText('foo'));
expect(onChangeMock).toHaveBeenCalledWith(['foo']);
onChangeMock.mockClear();

rerender(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={['foo']}
onChange={onChangeMock}
/>
);

await userEvent.click(screen.getByText('bar'));
expect(onChangeMock).toHaveBeenCalledWith(['foo', 'bar']);
onChangeMock.mockClear();

rerender(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={['foo', 'bar']}
value={['foo', 'bar']}
onChange={onChangeMock}
/>
);

await userEvent.click(screen.getByText('bar'));
expect(onChangeMock).toHaveBeenCalledWith(['foo']);
onChangeMock.mockClear();
});

it('should call obChange with option.value after option click', async () => {
const onChangeMock = jest.fn();
render(
<OptionsFilter
name="Tags"
searchPlaceholder="Search Tags"
options={[
{ name: 'foo', value: 1 },
{ name: 'bar', value: 2 },
]}
value={[]}
onChange={onChangeMock}
/>
);

expect(onChangeMock).not.toHaveBeenCalled();

await userEvent.click(screen.getByText('Tags'));
await userEvent.click(screen.getByText('foo'));
expect(onChangeMock).toHaveBeenCalledWith([1]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import userEvent from '@testing-library/user-event';

import { OptionsFilterItem } from '../options_filter_item';

import { render, screen } from '../../../../../test/test_utils';

describe('<OptionsFilterItem />', () => {
it('should render passed children and check icon', () => {
render(
<OptionsFilterItem checked="on" value="foo" onClick={() => {}}>
foo
</OptionsFilterItem>
);
expect(screen.getByText('foo')).toBeInTheDocument();
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
});

it('should call onClick with "foo" after click', async () => {
const onClickMock = jest.fn();
render(
<OptionsFilterItem checked="on" value="foo" onClick={onClickMock}>
foo
</OptionsFilterItem>
);
await userEvent.click(screen.getByRole('option'));
expect(onClickMock).toHaveBeenCalledWith('foo');
});
});
6 changes: 6 additions & 0 deletions public/components/common/options_filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { OptionsFilter, OptionsFilterProps } from './options_filter';
Loading