Skip to content

Commit

Permalink
fix: separate api auth to plugin folder (flyteorg#495)
Browse files Browse the repository at this point in the history
* fix: separate api auth to plugin folder
* chore: for local admin URL is undefined
* chore: update README.md
* test: fix tests + add new ones
* v0.0.2 - release for flyte-api plugin

Signed-off-by: Nastya <[email protected]>
  • Loading branch information
anrusina authored May 25, 2022
1 parent dd74c71 commit 271cb65
Show file tree
Hide file tree
Showing 34 changed files with 550 additions and 345 deletions.
6 changes: 3 additions & 3 deletions packages/composites/ui-atoms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.1-rc.2",
"description": "Flyteconsole UI atoms, which didn't plan to be published and would be consumed as is internally",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm",
"build:esm": "tsc --module esnext --outDir lib",
"build:cjs": "tsc",
"test": "NODE_ENV=test jest"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/plugins/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.1-rc.2",
"description": "Flyteconsole Components module, which is published as npm package and can be consumed by 3rd parties",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm --project ./tsconfig.build.json",
"build:esm": "tsc --module esnext --outDir lib --project ./tsconfig.build.json",
"build:cjs": "tsc --project ./tsconfig.build.json",
"test": "NODE_ENV=test jest"
},
Expand Down
40 changes: 39 additions & 1 deletion packages/plugins/flyte-api/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
This is a flyte-API package for flyteconsole plugin system
## @flyteconsole/flyte-api

This package provides ability to do FlyteAdmin API calls from JS/TS code.

At this point it allows to get though authentication steps, request user profile and FlyteAdmin version.
In future releases we will add ability to do all types of FlyteAdmin API calls.

### Installation

To install the package please run:
```bash
yarn add @flyteconsole/flyte-api
```

### Usage

To use in your application

- Wrap parent component with <FlyteApiProvider flyteApiDomain={ADMIN_API_URL ?? ''}>

`ADMIN_API_URL` is a flyte admin domain URL to which `/api/v1/_endpoint` part would be added, to perform REST API call.
`
Then from any child component

```js
import useAxios from 'axios-hooks';
import { useFlyteApi, defaultAxiosConfig } from '@flyteconsole/flyte-api';

...
/** Get profile information */
const apiContext = useFlyteApi();

const profilePath = apiContext.getProfileUrl();
const [{ data: profile, loading }] = useAxios({
url: profilePath,
method: 'GET',
...defaultAxiosConfig,
});
```
16 changes: 9 additions & 7 deletions packages/plugins/flyte-api/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
{
"name": "@flyteconsole/flyte-api",
"version": "0.0.1-rc.1",
"version": "0.0.2",
"description": "FlyteConsole plugin to allow access FlyteAPI",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"clean": "rm -rf dist && rm -rf lib",
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm --project ./tsconfig.build.json",
"build:esm": "tsc --module esnext --outDir lib --project ./tsconfig.build.json",
"build:cjs": "tsc --project ./tsconfig.build.json",
"push:update": "yarn clean && yarn build && yarn publish",
"test": "NODE_ENV=test jest"
},
"dependencies": {
"@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0",
"classnames": "^2.3.1"
"axios": "^0.27.2",
"camelcase-keys": "^7.0.2",
"snakecase-keys": "^5.4.2"
},
"peerDependencies": {
"react": "^16.13.1",
Expand Down
47 changes: 47 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/apiProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { FlyteApiProvider, useFlyteApi } from '.';
import { AdminEndpoint } from '../utils/constants';
import { getLoginUrl } from './login';

const MockCoponent = () => {
const context = useFlyteApi();

return (
<>
<div>{context.getProfileUrl()}</div>
<div>{context.getAdminApiUrl('/magic')}</div>
<div>{context.getLoginUrl()}</div>
</>
);
};

describe('fltyte-api/ApiProvider', () => {
it('getLoginUrl properly adds redirect url', () => {
const result = getLoginUrl(AdminEndpoint.Version, `http://some.nonsense`);
expect(result).toEqual('/version/login?redirect_url=http://some.nonsense');
});

it('If FlyteApiContext is not defined, returned URL uses default value', () => {
const { getAllByText } = render(<MockCoponent />);
expect(getAllByText('#').length).toBe(3);
});

it('If FlyteApiContext is defined, but flyteApiDomain is not point to localhost', () => {
const { getByText } = render(
<FlyteApiProvider>
<MockCoponent />
</FlyteApiProvider>,
);
expect(getByText('http://localhost/me')).toBeInTheDocument();
});

it('If FlyteApiContext provides flyteApiDomain value', () => {
const { getByText } = render(
<FlyteApiProvider flyteApiDomain="https://some.domain.here">
<MockCoponent />
</FlyteApiProvider>,
);
expect(getByText('https://some.domain.here/me')).toBeInTheDocument();
});
});
56 changes: 56 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import { createContext, useContext } from 'react';
import { getAdminApiUrl, getEndpointUrl } from '../utils';
import { AdminEndpoint, RawEndpoint } from '../utils/constants';
import { defaultLoginStatus, getLoginUrl, LoginStatus } from './login';

export interface FlyteApiContextState {
loginStatus: LoginStatus;
getLoginUrl: (redirect?: string) => string;
getProfileUrl: () => string;
getAdminApiUrl: (endpoint: AdminEndpoint | string) => string;
}

const FlyteApiContext = createContext<FlyteApiContextState>({
// default values - used when Provider wrapper is not found
loginStatus: defaultLoginStatus,
getLoginUrl: () => '#',
getProfileUrl: () => '#',
getAdminApiUrl: () => '#',
});

interface FlyteApiProviderProps {
flyteApiDomain?: string;
children?: React.ReactNode;
}

export const useFlyteApi = () => useContext(FlyteApiContext);

export const FlyteApiProvider = (props: FlyteApiProviderProps) => {
const { flyteApiDomain } = props;

const [loginExpired, setLoginExpired] = React.useState(false);

// Whenever we detect expired credentials, trigger a login redirect automatically
React.useEffect(() => {
if (loginExpired) {
window.location.href = getLoginUrl(flyteApiDomain);
}
}, [loginExpired]);

return (
<FlyteApiContext.Provider
value={{
loginStatus: {
expired: loginExpired,
setExpired: setLoginExpired,
},
getLoginUrl: (redirect) => getLoginUrl(flyteApiDomain, redirect),
getProfileUrl: () => getEndpointUrl(RawEndpoint.Profile, flyteApiDomain),
getAdminApiUrl: (endpoint) => getAdminApiUrl(endpoint, flyteApiDomain),
}}
>
{props.children}
</FlyteApiContext.Provider>
);
};
22 changes: 22 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getEndpointUrl } from '../utils';
import { RawEndpoint } from '../utils/constants';

export interface LoginStatus {
expired: boolean;
setExpired(expired: boolean): void;
}

export const defaultLoginStatus: LoginStatus = {
expired: true,
setExpired: () => {
/** Do nothing */
},
};

/** Constructs a url for redirecting to the Admin login endpoint and returning
* to the current location after completing the flow.
*/
export function getLoginUrl(adminUrl?: string, redirectUrl: string = window.location.href) {
const baseUrl = getEndpointUrl(RawEndpoint.Login, adminUrl);
return `${baseUrl}?redirect_url=${redirectUrl}`;
}
41 changes: 0 additions & 41 deletions packages/plugins/flyte-api/src/Sample/index.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions packages/plugins/flyte-api/src/Sample/sample.stories.tsx

This file was deleted.

11 changes: 0 additions & 11 deletions packages/plugins/flyte-api/src/Sample/sample.test.tsx

This file was deleted.

5 changes: 4 additions & 1 deletion packages/plugins/flyte-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { SampleComponent } from './Sample';
export { FlyteApiProvider, useFlyteApi, type FlyteApiContextState } from './ApiProvider';

export { AdminEndpoint, RawEndpoint } from './utils/constants';
export { getAxiosApiCall, defaultAxiosConfig } from './utils';
10 changes: 10 additions & 0 deletions packages/plugins/flyte-api/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum RawEndpoint {
Login = '/login',
Profile = '/me',
}

export const adminApiPrefix = '/api/v1';

export enum AdminEndpoint {
Version = '/version',
}
36 changes: 36 additions & 0 deletions packages/plugins/flyte-api/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable max-classes-per-file */
import { AxiosError } from 'axios';

export class NotFoundError extends Error {
constructor(public override name: string, msg = 'The requested item could not be found') {
super(msg);
}
}

/** Indicates failure to fetch a resource because the user is not authorized (401) */
export class NotAuthorizedError extends Error {
constructor(msg = 'User is not authorized to view this resource') {
super(msg);
}
}

/** Detects special cases for errors returned from Axios and lets others pass through. */
export function transformRequestError(err: unknown, path: string) {
const error = err as AxiosError;

if (!error.response) {
return error;
}

// For some status codes, we'll throw a special error to allow
// client code and components to handle separately
if (error.response.status === 404) {
return new NotFoundError(path);
}
if (error.response.status === 401) {
return new NotAuthorizedError();
}

// this error is not decoded.
return error;
}
Loading

0 comments on commit 271cb65

Please sign in to comment.