diff --git a/frontend/amundsen_application/static/.betterer.results b/frontend/amundsen_application/static/.betterer.results index de69d671e4..0431ac8176 100644 --- a/frontend/amundsen_application/static/.betterer.results +++ b/frontend/amundsen_application/static/.betterer.results @@ -505,17 +505,14 @@ exports[`eslint`] = { "js/components/Tags/index.tsx:3468508233": [ [38, 4, 21, "Must use destructuring props assignment", "4236634811"] ], - "js/config/config-default.ts:1497564975": [ + "js/config/config-default.ts:54755128": [ [2, 0, 72, "\`../interfaces\` import should occur before import of \`./config-types\`", "1449508543"], - [236, 6, 21, "\'partitionKey\' is defined but never used.", "399589312"], - [237, 6, 23, "\'partitionValue\' is defined but never used.", "793372348"] + [281, 6, 21, "\'partitionKey\' is defined but never used.", "399589312"], + [282, 6, 23, "\'partitionValue\' is defined but never used.", "793372348"] ], "js/config/config-utils.ts:2658622130": [ [11, 0, 45, "\`../interfaces\` import should occur before import of \`./config-types\`", "3885176344"] ], - "js/ducks/announcements/api/v0.ts:908063915": [ - [30, 13, 64, "Expected the Promise rejection reason to be an Error.", "3665078104"] - ], "js/ducks/announcements/index.spec.ts:1898496537": [ [3, 0, 148, "\`.\` import should occur after import of \`./types\`", "4154971894"] ], @@ -542,8 +539,7 @@ exports[`eslint`] = { [0, 16, 13, "\'AxiosResponse\' is defined but never used.", "1743879434"] ], "js/ducks/dashboard/api/v0.ts:2755958463": [ - [38, 28, 104, "Do not nest ternary expressions.", "1163212497"], - [45, 13, 68, "Expected the Promise rejection reason to be an Error.", "2764124112"] + [38, 28, 104, "Do not nest ternary expressions.", "1163212497"] ], "js/ducks/issue/reducer.ts:1774302197": [ [119, 6, 56, "Unexpected lexical declaration in case block.", "2031834906"] @@ -557,16 +553,9 @@ exports[`eslint`] = { [250, 8, 31, "Use object destructuring.", "507617405"], [251, 8, 45, "Use object destructuring.", "244310461"] ], - "js/ducks/lastIndexed/api/v0.ts:100101993": [ - [18, 13, 44, "Expected the Promise rejection reason to be an Error.", "107682302"] - ], "js/ducks/lastIndexed/sagas.ts:1498244597": [ [7, 2, 29, "\'action\' is defined but never used.", "566797395"] ], - "js/ducks/lineage/api/v0.ts:63450243": [ - [19, 13, 26, "Expected the Promise rejection reason to be an Error.", "875716520"], - [44, 13, 26, "Expected the Promise rejection reason to be an Error.", "875716520"] - ], "js/ducks/middlewares/analyticsMiddleware.ts:3943673455": [ [11, 7, 8, "\'getState\' is defined but never used.", "1919118020"] ], @@ -594,9 +583,7 @@ exports[`eslint`] = { ], "js/ducks/tableMetadata/api/v0.ts:1722284731": [ [75, 8, 23, "Use object destructuring.", "1142306891"], - [78, 13, 39, "Expected the Promise rejection reason to be an Error.", "3871091426"], - [125, 23, -4008, "Expected to return a value at the end of arrow function.", "5381"], - [181, 13, 32, "Expected the Promise rejection reason to be an Error.", "2773728948"] + [125, 23, -4008, "Expected to return a value at the end of arrow function.", "5381"] ], "js/ducks/tableMetadata/index.spec.ts:2466548220": [ [480, 22, 11, "\'mockSuccess\' is already declared in the upper scope.", "1120045516"], @@ -1003,7 +990,7 @@ exports[`eslint`] = { [208, 14, 151, "A control must be associated with a text label.", "1015004405"], [218, 12, 432, "A control must be associated with a text label.", "534436217"] ], - "js/pages/TableDetailPage/SourceLink/index.spec.tsx:4268867941": [ + "js/pages/TableDetailPage/SourceLink/index.spec.tsx:3686659452": [ [19, 55, 10, "Prop spreading is forbidden", "480399587"] ], "js/pages/TableDetailPage/TableHeaderBullets/index.spec.tsx:3978961134": [ @@ -1011,7 +998,7 @@ exports[`eslint`] = { [91, 6, 25, "Use object destructuring.", "354229464"], [92, 6, 29, "Use object destructuring.", "2645724888"] ], - "js/pages/TableDetailPage/TableHeaderBullets/index.tsx:3687188386": [ + "js/pages/TableDetailPage/TableHeaderBullets/index.tsx:1080737806": [ [35, 25, 19, "Must use destructuring props assignment", "2861102024"], [40, 4, 25, "Must use destructuring props assignment", "2288791878"], [45, 6, 17, "Must use destructuring props assignment", "2741866010"], diff --git a/frontend/amundsen_application/static/.betterer.ts b/frontend/amundsen_application/static/.betterer.ts index cca1dfc6ed..535aa25605 100644 --- a/frontend/amundsen_application/static/.betterer.ts +++ b/frontend/amundsen_application/static/.betterer.ts @@ -22,7 +22,6 @@ export default { 'no-useless-return': 'error', 'no-void': 'error', 'prefer-destructuring': 'error', - 'prefer-promise-reject-errors': 'error', 'react/button-has-type': 'error', 'react/destructuring-assignment': 'error', 'react/jsx-boolean-value': 'error', diff --git a/frontend/amundsen_application/static/js/config/config-default.ts b/frontend/amundsen_application/static/js/config/config-default.ts index 9d66c3737f..37bef68431 100644 --- a/frontend/amundsen_application/static/js/config/config-default.ts +++ b/frontend/amundsen_application/static/js/config/config-default.ts @@ -104,6 +104,47 @@ const configDefault: AppConfig = { ], notices: {}, }, + [ResourceType.feature]: { + displayName: 'ML Features', + supportedSources: { + bigquery: { + displayName: 'BigQuery', + iconClass: 'icon-bigquery', + }, + delta: { + displayName: 'Delta', + iconClass: 'icon-delta', + }, + dremio: { + displayName: 'Dremio', + iconClass: 'icon-dremio', + }, + druid: { + displayName: 'Druid', + iconClass: 'icon-druid', + }, + hive: { + displayName: 'Hive', + iconClass: 'icon-hive', + }, + presto: { + displayName: 'Presto', + iconClass: 'icon-presto', + }, + postgres: { + displayName: 'Postgres', + iconClass: 'icon-postgres', + }, + redshift: { + displayName: 'Redshift', + iconClass: 'icon-redshift', + }, + snowflake: { + displayName: 'Snowflake', + iconClass: 'icon-snowflake', + }, + }, + }, [ResourceType.table]: { displayName: 'Datasets', supportedSources: { diff --git a/frontend/amundsen_application/static/js/config/config-types.ts b/frontend/amundsen_application/static/js/config/config-types.ts index 1d091f63ae..2a9283742d 100644 --- a/frontend/amundsen_application/static/js/config/config-types.ts +++ b/frontend/amundsen_application/static/js/config/config-types.ts @@ -225,6 +225,7 @@ interface ResourceConfig { [ResourceType.dashboard]: BaseResourceConfig; [ResourceType.table]: TableResourceConfig; [ResourceType.user]: BaseResourceConfig; + [ResourceType.feature]: BaseResourceConfig; } /** diff --git a/frontend/amundsen_application/static/js/ducks/feature/api/api.spec.ts b/frontend/amundsen_application/static/js/ducks/feature/api/api.spec.ts new file mode 100644 index 0000000000..6faeea72f7 --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/api/api.spec.ts @@ -0,0 +1,56 @@ +import axios from 'axios'; + +import { featureMetadata } from 'fixtures/metadata/feature'; + +import * as API from './v0'; + +jest.mock('axios'); + +describe('getFeature', () => { + let axiosMockGet; + it('resolves with object containing feature metadata and status code', async () => { + const mockStatus = 200; + const mockResponse = { + data: { + featureData: featureMetadata, + msg: 'success', + }, + status: mockStatus, + }; + axiosMockGet = jest + .spyOn(axios, 'get') + .mockImplementationOnce(() => Promise.resolve(mockResponse)); + expect.assertions(2); + await API.getFeature('testUri').then((processedResponse) => { + expect(processedResponse).toEqual({ + feature: featureMetadata, + statusCode: mockStatus, + }); + }); + expect(axiosMockGet).toHaveBeenCalled(); + }); + + it('catches error and resolves with object containing error information', async () => { + const mockStatus = 500; + const mockMessage = 'oops'; + const mockResponse = { + response: { + data: { + msg: mockMessage, + }, + status: mockStatus, + }, + }; + axiosMockGet = jest + .spyOn(axios, 'get') + .mockImplementationOnce(() => Promise.reject(mockResponse)); + expect.assertions(2); + await API.getFeature('testUri').catch((processedResponse) => { + expect(processedResponse).toEqual({ + statusMessage: mockMessage, + statusCode: mockStatus, + }); + }); + expect(axiosMockGet).toHaveBeenCalled(); + }); +}); diff --git a/frontend/amundsen_application/static/js/ducks/feature/api/v0.ts b/frontend/amundsen_application/static/js/ducks/feature/api/v0.ts new file mode 100644 index 0000000000..4f2d9a54c5 --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/api/v0.ts @@ -0,0 +1,33 @@ +import axios, { AxiosResponse } from 'axios'; +import * as qs from 'simple-query-string'; + +import { FeatureMetadata } from 'interfaces/Feature'; + +export type GetFeatureAPI = { + msg: string; + featureData: FeatureMetadata; +}; + +const FEATURE_BASE = '/api/metadata/v0'; + +export function getFeature(key: string, index?: string, source?: string) { + const queryParams = qs.stringify({ key, index, source }); + return axios + .get(`${FEATURE_BASE}/feature?${queryParams}`) + .then((response: AxiosResponse) => { + const { data, status } = response; + return { + feature: data.featureData, + statusCode: status, + }; + }) + .catch((e) => { + const { response } = e; + const statusMessage = response.data?.msg; + const statusCode = response?.status || 500; + return Promise.reject({ + statusCode, + statusMessage, + }); + }); +} diff --git a/frontend/amundsen_application/static/js/ducks/feature/reducer.spec.ts b/frontend/amundsen_application/static/js/ducks/feature/reducer.spec.ts new file mode 100644 index 0000000000..f8ef127c8c --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/reducer.spec.ts @@ -0,0 +1,62 @@ +import reducer, { + getFeature, + getFeatureFailure, + getFeatureSuccess, + FeatureReducerState, + initialFeatureState, +} from 'ducks/feature/reducer'; +import { featureMetadata } from '../../fixtures/metadata/feature'; + +describe('feature reducer', () => { + let testState: FeatureReducerState; + beforeEach(() => { + testState = { + isLoading: false, + statusCode: 200, + feature: initialFeatureState, + }; + }); + + it('should return the existing state if action is not handled', () => { + expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState); + }); + + it('should handle getFeature.REQUEST', () => { + expect(reducer(testState, getFeature('testKey'))).toEqual({ + ...testState, + isLoading: true, + statusCode: null, + }); + }); + + it('should handle GetFeature.SUCCESS', () => { + expect( + reducer( + testState, + getFeatureSuccess({ + feature: featureMetadata, + statusCode: 202, + }) + ) + ).toEqual({ + isLoading: false, + statusCode: 202, + feature: featureMetadata, + }); + }); + + it('should handle GetFeature.FAILURE', () => { + expect( + reducer( + testState, + getFeatureFailure({ + statusCode: 500, + }) + ) + ).toEqual({ + isLoading: false, + statusCode: 500, + feature: initialFeatureState, + }); + }); +}); diff --git a/frontend/amundsen_application/static/js/ducks/feature/reducer.ts b/frontend/amundsen_application/static/js/ducks/feature/reducer.ts new file mode 100644 index 0000000000..2b3b37fcad --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/reducer.ts @@ -0,0 +1,105 @@ +import { + GetFeature, + GetFeatureRequest, + GetFeatureResponse, + GetFeaturePayload, +} from 'ducks/feature/types'; +import { FeatureMetadata } from 'interfaces/Feature'; + +/* Actions */ + +export function getFeature( + key: string, + index?: string, + source?: string +): GetFeatureRequest { + return { + payload: { + key, + index, + source, + }, + type: GetFeature.REQUEST, + }; +} + +export function getFeatureSuccess(payload: GetFeaturePayload) { + return { + payload, + type: GetFeature.SUCCESS, + }; +} + +export function getFeatureFailure( + payload: GetFeaturePayload +): GetFeatureResponse { + return { + payload, + type: GetFeature.FAILURE, + }; +} + +/* Reducer */ + +export interface FeatureReducerState { + isLoading: boolean; + statusCode: number | null; + feature: FeatureMetadata; +} + +export const initialFeatureState: FeatureMetadata = { + key: '', + name: '', + version: '', + status: '', + feature_group: '', + entity: [], + data_type: '', + availability: [], + description: '', + owners: [], + badges: [], + owner_tags: [], + tags: [], + programmatic_descriptions: [], + watermarks: [], + stats: [], + last_updated_timestamp: 0, + created_timestamp: 0, +}; + +export const initialState: FeatureReducerState = { + isLoading: true, + statusCode: null, + feature: initialFeatureState, +}; + +export default function reducer( + state: FeatureReducerState = initialState, + action +): FeatureReducerState { + switch (action.type) { + case GetFeature.REQUEST: + return { + ...state, + statusCode: null, + isLoading: true, + }; + case GetFeature.FAILURE: + return { + ...state, + isLoading: false, + statusCode: action.payload.statusCode, + feature: initialFeatureState, + }; + case GetFeature.SUCCESS: + return { + ...state, + isLoading: false, + statusCode: action.payload.statusCode, + feature: action.payload.feature, + }; + default: + return state; + } +} diff --git a/frontend/amundsen_application/static/js/ducks/feature/sagas.spec.ts b/frontend/amundsen_application/static/js/ducks/feature/sagas.spec.ts new file mode 100644 index 0000000000..e9c732576b --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/sagas.spec.ts @@ -0,0 +1,51 @@ +import { testSaga } from 'redux-saga-test-plan'; + +import { featureMetadata } from 'fixtures/metadata/feature'; +import * as API from './api/v0'; +import * as Sagas from './sagas'; + +import { getFeature, getFeatureFailure, getFeatureSuccess } from './reducer'; +import { GetFeature } from './types'; + +describe('feature sagas', () => { + describe('getFeatureWatcher', () => { + it('takes every GetFeature.REQUEST with getFeatureWorker', () => { + testSaga(Sagas.getFeatureWatcher) + .next() + .takeEvery(GetFeature.REQUEST, Sagas.getFeatureWorker) + .next() + .isDone(); + }); + }); + + describe('getFeatureWorker', () => { + it('executes flow for successfully getting a feature', () => { + const mockResponse = { + feature: featureMetadata, + statusCode: 200, + }; + testSaga(Sagas.getFeatureWorker, getFeature('testUri', '0', 'source')) + .next() + .call(API.getFeature, 'testUri', '0', 'source') + .next(mockResponse) + .put(getFeatureSuccess(mockResponse)) + .next() + .isDone(); + }); + + it('executes flow for a failed request feature', () => { + const mockResponse = { + statusCode: 200, + statusMessage: 'oops', + }; + testSaga(Sagas.getFeatureWorker, getFeature('testUri', '0', 'source')) + .next() + .call(API.getFeature, 'testUri', '0', 'source') + // @ts-ignore + .throw(mockResponse) + .put(getFeatureFailure(mockResponse)) + .next() + .isDone(); + }); + }); +}); diff --git a/frontend/amundsen_application/static/js/ducks/feature/sagas.ts b/frontend/amundsen_application/static/js/ducks/feature/sagas.ts new file mode 100644 index 0000000000..5095ec0aa2 --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/sagas.ts @@ -0,0 +1,21 @@ +import { SagaIterator } from 'redux-saga'; +import { call, put, takeEvery } from 'redux-saga/effects'; + +import * as API from './api/v0'; +import { getFeatureSuccess, getFeatureFailure } from './reducer'; +import { GetFeature } from './types'; + +export function* getFeatureWorker(action): SagaIterator { + try { + const { key, index, source } = action.payload; + const response = yield call(API.getFeature, key, index, source); + + yield put(getFeatureSuccess(response)); + } catch (error) { + yield put(getFeatureFailure(error)); + } +} + +export function* getFeatureWatcher(): SagaIterator { + yield takeEvery(GetFeature.REQUEST, getFeatureWorker); +} diff --git a/frontend/amundsen_application/static/js/ducks/feature/types.ts b/frontend/amundsen_application/static/js/ducks/feature/types.ts new file mode 100644 index 0000000000..26741f363b --- /dev/null +++ b/frontend/amundsen_application/static/js/ducks/feature/types.ts @@ -0,0 +1,27 @@ +import { FeatureMetadata } from 'interfaces/Feature'; + +export enum GetFeature { + REQUEST = 'amundsen/feature/GET_FEATURE_REQUEST', + SUCCESS = 'amundsen/feature/GET_FEATURE_SUCCESS', + FAILURE = 'amundsen/feature/GET_FEATURE_FAILURE', +} + +export interface GetFeatureRequest { + type: GetFeature.REQUEST; + payload: { + key: string; + index?: string; + source?: string; + }; +} + +export interface GetFeatureResponse { + type: GetFeature.SUCCESS | GetFeature.FAILURE; + payload: GetFeaturePayload; +} + +export interface GetFeaturePayload { + feature?: FeatureMetadata; + statusCode?: number; + statusMessage?: string; +} diff --git a/frontend/amundsen_application/static/js/ducks/rootReducer.ts b/frontend/amundsen_application/static/js/ducks/rootReducer.ts index 74d3ebe0ea..9c86341263 100644 --- a/frontend/amundsen_application/static/js/ducks/rootReducer.ts +++ b/frontend/amundsen_application/static/js/ducks/rootReducer.ts @@ -4,6 +4,7 @@ import { combineReducers } from 'redux'; import dashboard, { DashboardReducerState } from 'ducks/dashboard/reducer'; +import feature, { FeatureReducerState } from 'ducks/feature/reducer'; import announcements, { AnnouncementsReducerState } from './announcements'; import feedback, { FeedbackReducerState } from './feedback/reducer'; import popularTables, { @@ -26,6 +27,7 @@ export interface GlobalState { announcements: AnnouncementsReducerState; bookmarks: BookmarkReducerState; dashboard: DashboardReducerState; + feature: FeatureReducerState; feedback: FeedbackReducerState; issue: IssueReducerState; notification: NotificationReducerState; @@ -43,6 +45,7 @@ const rootReducer = combineReducers({ announcements, bookmarks, dashboard, + feature, feedback, issue, notification, diff --git a/frontend/amundsen_application/static/js/ducks/rootSaga.ts b/frontend/amundsen_application/static/js/ducks/rootSaga.ts index 30d0fc1610..f08261e1e1 100644 --- a/frontend/amundsen_application/static/js/ducks/rootSaga.ts +++ b/frontend/amundsen_application/static/js/ducks/rootSaga.ts @@ -11,11 +11,14 @@ import { // Dashboard import { getDashboardWatcher } from 'ducks/dashboard/sagas'; +import { getFeatureWatcher } from 'ducks/feature/sagas'; import { getAnnouncementsWatcher } from './announcements/sagas'; // Notifications import { submitNotificationWatcher } from './notification/sagas'; +// Feature + // FeedbackForm import { submitFeedbackWatcher } from './feedback/sagas'; @@ -84,6 +87,8 @@ export default function* rootSaga() { getDashboardWatcher(), // Notification submitNotificationWatcher(), + // Feature + getFeatureWatcher(), // FeedbackForm submitFeedbackWatcher(), // Issues diff --git a/frontend/amundsen_application/static/js/fixtures/globalState.ts b/frontend/amundsen_application/static/js/fixtures/globalState.ts index 0ba36f4f9e..97ba353271 100644 --- a/frontend/amundsen_application/static/js/fixtures/globalState.ts +++ b/frontend/amundsen_application/static/js/fixtures/globalState.ts @@ -65,6 +65,30 @@ const globalState: GlobalState = { statusCode: 200, dashboard: dashboardMetadata, }, + feature: { + statusCode: 200, + isLoading: false, + feature: { + key: '', + name: '', + version: '', + status: '', + feature_group: '', + entity: [], + data_type: '', + availability: [], + description: '', + owners: [], + badges: [], + owner_tags: [], + tags: [], + programmatic_descriptions: [], + watermarks: [], + stats: [], + last_updated_timestamp: 0, + created_timestamp: 0, + }, + }, feedback: { sendState: SendingState.IDLE, }, diff --git a/frontend/amundsen_application/static/js/fixtures/metadata/feature.ts b/frontend/amundsen_application/static/js/fixtures/metadata/feature.ts new file mode 100644 index 0000000000..6e32b96a75 --- /dev/null +++ b/frontend/amundsen_application/static/js/fixtures/metadata/feature.ts @@ -0,0 +1,38 @@ +import { FeatureMetadata, FeatureSummary } from 'interfaces/Feature'; + +export const featureSummary: FeatureSummary = { + key: 'test key', + name: 'test feature name', + version: '1.02.0', + availability: ['source 1', 'source 2'], + entity: ['entity 1', 'entity 2'], + description: 'test feature description', +}; + +export const featureMetadata: FeatureMetadata = { + key: 'test key', + name: 'test feature name', + version: '1.02.0', + status: 'status', + feature_group: 'feature group', + entity: ['entity 1', 'entity 2'], + data_type: 'string', + availability: ['source 1', 'source 2'], + description: 'test feature description', + owners: [ + { + display_name: 'test', + email: 'test@email.com', + profile_url: 'profile_url', + user_id: 'user_id', + }, + ], + badges: [], + owner_tags: [], + tags: [], + programmatic_descriptions: [], + watermarks: [], + stats: [], + last_updated_timestamp: 0, + created_timestamp: 0, +}; diff --git a/frontend/amundsen_application/static/js/index.tsx b/frontend/amundsen_application/static/js/index.tsx index 5ca40a882f..60e0161b54 100644 --- a/frontend/amundsen_application/static/js/index.tsx +++ b/frontend/amundsen_application/static/js/index.tsx @@ -22,9 +22,10 @@ import { pageViewed } from 'ducks/ui'; import rootReducer from 'ducks/rootReducer'; import rootSaga from 'ducks/rootSaga'; -import DashboardPage from './pages/DashboardPage'; import AnnouncementPage from './pages/AnnouncementPage'; import BrowsePage from './pages/BrowsePage'; +import DashboardPage from './pages/DashboardPage'; +import FeaturePage from './pages/FeaturePage'; import HomePage from './pages/HomePage'; import NotFoundPage from './pages/NotFoundPage'; import SearchPage from './pages/SearchPage'; @@ -64,6 +65,7 @@ const Routes: React.FC = () => { + GetFeatureRequest; +} + +interface FeatureRouteParams { + group: string; + name: string; + version: string; +} + +export type FeaturePageProps = RouteComponentProps & + StateFromProps & + DispatchFromProps; + +export function renderTabs() { + return null; +} + +const FeaturePageLoader: React.FC = () => ( +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); + +const getFeatureKey = (group: string, name: string, version: string) => + `${group}/${name}/${version}`; + +const FeaturePage: React.FC = ({ + isLoading, + feature, + getFeatureDispatch, + location, + match, +}: FeaturePageProps) => { + const [key, setKey] = React.useState(''); + React.useEffect(() => { + const { group, name, version } = match.params; + const newKey = getFeatureKey(group, name, version); + if (key !== newKey) { + const { index, source } = getLoggingParams(location.search); + setKey(newKey); + getFeatureDispatch(newKey, index, source); + } + }); + + if (isLoading) { + return ; + } + const sourcesWithDisplay = feature.availability.map((source) => + getSourceDisplayName(source, ResourceType.feature) + ); + return ( +
+
+
+ + +
+
+

+ {feature.name} +

+
+ Feature •  + {sourcesWithDisplay.join(', ')} +
+
+
+
+ +
{renderTabs()}
+
+
+ ); +}; + +export const mapStateToProps = (state: GlobalState) => ({ + isLoading: state.feature.isLoading, + statusCode: state.feature.statusCode, + feature: state.feature.feature, +}); + +export const mapDispatchToProps = (dispatch: any) => + bindActionCreators({ getFeatureDispatch: getFeature }, dispatch); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(FeaturePage); diff --git a/frontend/amundsen_application/static/js/pages/FeaturePage/styles.scss b/frontend/amundsen_application/static/js/pages/FeaturePage/styles.scss new file mode 100644 index 0000000000..0a4d575218 --- /dev/null +++ b/frontend/amundsen_application/static/js/pages/FeaturePage/styles.scss @@ -0,0 +1,45 @@ +// Copyright Contributors to the Amundsen project. +// SPDX-License-Identifier: Apache-2.0 + +@import 'variables'; + +.feature-page { + .shimmer-page-title { + height: 24px; + width: 600px; + margin-bottom: $spacer-1; + } + + .shimmer-page-subtitle { + height: 18px; + width: 200px; + } + + .shimmer-section-title { + height: 18px; + width: 100%; + margin-bottom: $spacer-1; + } + + .shimmer-section-content { + height: 32px; + width: 100%; + } + + .shimmer-tab-title { + display: flex; + flex-direction: row; + + .shimmer-tab { + height: 32px; + width: 100px; + margin: $spacer-2 $spacer-2 $spacer-1; + } + } + + .shimmer-tab-content { + height: 50px; + width: 100%; + margin-bottom: $spacer-1; + } +} diff --git a/frontend/amundsen_application/static/js/pages/TableDetailPage/SourceLink/index.spec.tsx b/frontend/amundsen_application/static/js/pages/TableDetailPage/SourceLink/index.spec.tsx index 7f8fff3933..53e5242f69 100644 --- a/frontend/amundsen_application/static/js/pages/TableDetailPage/SourceLink/index.spec.tsx +++ b/frontend/amundsen_application/static/js/pages/TableDetailPage/SourceLink/index.spec.tsx @@ -68,6 +68,9 @@ describe('render SourceLink', () => { [ResourceType.dashboard]: { displayName: 'Dashboards', }, + [ResourceType.feature]: { + displayName: 'ML Features', + }, [ResourceType.user]: { displayName: 'Users', },