Skip to content

Commit

Permalink
feat: support external models in deployed model list (#248) (#251)
Browse files Browse the repository at this point in the history
* feat: support external models in deployed model list

Signed-off-by: Lin Wang <wonglam@amazon.com>

* refactor: update name to required

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: connector index not found

Signed-off-by: Lin Wang <wonglam@amazon.com>

* refactor: update options filter with normal string[] value

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: hits not defined

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: update wording

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: connector id not exists in all connectors

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: show models when failed to load all external connectors

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: update deployed models title to models

Signed-off-by: Lin Wang <wonglam@amazon.com>

* refactor: remove unused code in model connector filter

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: address PR comments

Signed-off-by: Lin Wang <wonglam@amazon.com>

---------

Signed-off-by: Lin Wang <wonglam@amazon.com>
(cherry picked from commit cc3810b)

Co-authored-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
opensearch-trigger-bot[bot] and wanglam authored Aug 29, 2023
1 parent 55e9e38 commit 0710c49
Show file tree
Hide file tree
Showing 33 changed files with 1,302 additions and 138 deletions.
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

0 comments on commit 0710c49

Please sign in to comment.