Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into feature/slicing-pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
Huong Nguyen committed Jul 29, 2024
2 parents f49ab77 + f75f16a commit 0a335b8
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 4 deletions.
1 change: 1 addition & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Please follow the established format:
- Refactor namespace pipelines. (#1897)
- Expose the internal Redux state through `options` prop while using Kedro-Viz as a React component. (#1969)
- Enhance documentation for the Kedro-Viz standalone React component. (#1954)
- Add Datasets preview toggle in the settings panel. (#1977)

## Bug fixes and other changes

Expand Down
6 changes: 6 additions & 0 deletions package/kedro_viz/api/rest/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ class DeployerConfiguration(BaseModel):
is_all_previews_enabled: bool = False
endpoint: str
bucket_name: str


class UserPreference(BaseModel):
"""User preferences for Kedro Viz."""

showDatasetPreviews: bool
35 changes: 33 additions & 2 deletions package/kedro_viz/api/rest/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from fastapi import APIRouter
from fastapi.responses import JSONResponse

from kedro_viz.api.rest.requests import DeployerConfiguration
from kedro_viz.api.rest.requests import DeployerConfiguration, UserPreference
from kedro_viz.constants import PACKAGE_REQUIREMENTS
from kedro_viz.integrations.deployment.deployer_factory import DeployerFactory

from .responses import (
APIErrorMessage,
DataNodeMetadata,
GraphAPIResponse,
NodeMetadataAPIResponse,
PackageCompatibilityAPIResponse,
Expand Down Expand Up @@ -49,6 +50,36 @@ async def get_single_node_metadata(node_id: str):
return get_node_metadata_response(node_id)


@router.post("/preferences")
async def update_preferences(preferences: UserPreference):
try:
DataNodeMetadata.set_is_all_previews_enabled(preferences.showDatasetPreviews)
return JSONResponse(
status_code=200, content={"message": "Preferences updated successfully"}
)
except Exception as exception:
logger.error("Failed to update preferences: %s", str(exception))
return JSONResponse(
status_code=500,
content={"message": "Failed to update preferences"},
)


@router.get("/preferences", response_model=UserPreference)
async def get_preferences():
try:
show_dataset_previews = DataNodeMetadata.is_all_previews_enabled
return JSONResponse(
status_code=200, content={"showDatasetPreviews": show_dataset_previews}
)
except Exception as exception:
logger.error("Failed to fetch preferences: %s", str(exception))
return JSONResponse(
status_code=500,
content={"message": "Failed to fetch preferences"},
)


@router.get(
"/pipelines/{registered_pipeline_id}",
response_model=GraphAPIResponse,
Expand Down Expand Up @@ -99,7 +130,7 @@ async def get_package_compatibilities():
return get_package_compatibilities_response(PACKAGE_REQUIREMENTS)
except Exception as exc:
logger.exception(
"An exception occured while getting package compatibility info : %s", exc
"An exception occurred while getting package compatibility info : %s", exc
)
return JSONResponse(
status_code=500,
Expand Down
42 changes: 42 additions & 0 deletions package/tests/test_api/test_rest/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,45 @@ def test_get_package_compatibilities(

assert response.status_code == expected_status_code
assert response.json() == expected_response


def test_update_preferences_success(client, mocker):
mocker.patch(
"kedro_viz.api.rest.responses.DataNodeMetadata.set_is_all_previews_enabled"
)
response = client.post("api/preferences", json={"showDatasetPreviews": True})

assert response.status_code == 200
assert response.json() == {"message": "Preferences updated successfully"}


def test_update_preferences_failure(client, mocker):
mocker.patch(
"kedro_viz.api.rest.responses.DataNodeMetadata.set_is_all_previews_enabled",
side_effect=Exception("Test Exception"),
)
response = client.post("api/preferences", json={"showDatasetPreviews": True})

assert response.status_code == 500
assert response.json() == {"message": "Failed to update preferences"}


def test_get_preferences_success(client, mocker):
mocker.patch(
"kedro_viz.api.rest.responses.DataNodeMetadata", is_all_previews_enabled=True
)
response = client.get("/api/preferences")

assert response.status_code == 200
assert response.json() == {"showDatasetPreviews": True}


def test_get_preferences_failure(client, mocker):
mocker.patch(
"kedro_viz.api.rest.responses.DataNodeMetadata.is_all_previews_enabled",
side_effect=Exception("Test Exception"),
)
response = client.get("/api/preferences")

assert response.status_code == 500
assert response.json() == {"message": "Failed to fetch preferences"}
19 changes: 19 additions & 0 deletions src/actions/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { fetchPreferences } from '../utils/preferences-api';

// Action Types
export const UPDATE_USER_PREFERENCES = 'UPDATE_USER_PREFERENCES';

// Action Creators
export const updateUserPreferences = (preferences) => ({
type: UPDATE_USER_PREFERENCES,
payload: preferences,
});

export const getPreferences = () => async (dispatch) => {
try {
const preferences = await fetchPreferences();
dispatch(updateUserPreferences(preferences));
} catch (error) {
console.error('Error fetching preferences:', error);
}
};
55 changes: 53 additions & 2 deletions src/components/settings-modal/settings-modal.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { connect } from 'react-redux';
import {
changeFlag,
toggleShowFeatureHints,
toggleIsPrettyName,
toggleSettingsModal,
} from '../../actions';
import {
getPreferences,
updateUserPreferences,
} from '../../actions/preferences';
import { getFlagsState } from '../../utils/flags';
import SettingsModalRow from './settings-modal-row';
import { settings as settingsConfig, localStorageName } from '../../config';
import { saveLocalStorage } from '../../store/helpers';
import { localStorageKeyFeatureHintsStep } from '../../components/feature-hints/feature-hints';
import { updatePreferences } from '../../utils/preferences-api';

import Button from '../ui/button';
import Modal from '../ui/modal';
Expand All @@ -27,11 +32,14 @@ const SettingsModal = ({
showFeatureHints,
isOutdated,
isPrettyName,
showDatasetPreviews,
latestVersion,
onToggleFlag,
onToggleShowFeatureHints,
onToggleIsPrettyName,
onToggleShowDatasetPreviews,
showSettingsModal,
getPreferences,
visible,
}) => {
const flagData = getFlagsState();
Expand All @@ -40,12 +48,32 @@ const SettingsModal = ({
const [isPrettyNameValue, setIsPrettyName] = useState(isPrettyName);
const [showFeatureHintsValue, setShowFeatureHintsValue] =
useState(showFeatureHints);
const [showDatasetPreviewsValue, setShowDatasetPreviewsValue] =
useState(showDatasetPreviews);
const [toggleFlags, setToggleFlags] = useState(flags);

useEffect(() => {
setShowFeatureHintsValue(showFeatureHints);
}, [showFeatureHints]);

useEffect(() => {
setShowDatasetPreviewsValue(showDatasetPreviews);
}, [showDatasetPreviews]);

useEffect(() => {
if (visible.settingsModal) {
getPreferences();
}
}, [visible.settingsModal, getPreferences]);

const handleSavePreferences = useCallback(async () => {
try {
await updatePreferences(showDatasetPreviewsValue);
} catch (error) {
console.error('Error updating preferences:', error);
}
}, [showDatasetPreviewsValue]);

useEffect(() => {
let modalTimeout, resetTimeout;

Expand All @@ -63,8 +91,10 @@ const SettingsModal = ({
return onToggleFlag(name, value);
});

handleSavePreferences();
onToggleIsPrettyName(isPrettyNameValue);
onToggleShowFeatureHints(showFeatureHintsValue);
onToggleShowDatasetPreviews(showDatasetPreviewsValue);
setHasNotInteracted(true);
setHasClickApplyAndClose(false);

Expand All @@ -80,19 +110,23 @@ const SettingsModal = ({
hasClickedApplyAndClose,
showFeatureHintsValue,
isPrettyNameValue,
showDatasetPreviewsValue,
onToggleFlag,
onToggleShowFeatureHints,
onToggleIsPrettyName,
onToggleShowDatasetPreviews,
showSettingsModal,
toggleFlags,
handleSavePreferences,
]);

const resetStateCloseModal = () => {
showSettingsModal(false);
setHasNotInteracted(true);
setToggleFlags(flags);
setIsPrettyName(isPrettyName);
setShowFeatureHintsValue(showFeatureHintsValue);
setShowFeatureHintsValue(showFeatureHints);
setShowDatasetPreviewsValue(showDatasetPreviews);
};

return (
Expand Down Expand Up @@ -130,6 +164,16 @@ const SettingsModal = ({
}
}}
/>
<SettingsModalRow
id="showDatasetPreviews"
name={settingsConfig['showDatasetPreviews'].name}
toggleValue={showDatasetPreviewsValue}
description={settingsConfig['showDatasetPreviews'].description}
onToggleChange={(event) => {
setShowDatasetPreviewsValue(event.target.checked);
setHasNotInteracted(false);
}}
/>
{flagData.map(({ name, value, description }) => (
<SettingsModalRow
description={description}
Expand Down Expand Up @@ -209,13 +253,17 @@ export const mapStateToProps = (state) => ({
flags: state.flags,
showFeatureHints: state.showFeatureHints,
isPrettyName: state.isPrettyName,
showDatasetPreviews: state.userPreferences.showDatasetPreviews,
visible: state.visible,
});

export const mapDispatchToProps = (dispatch) => ({
showSettingsModal: (value) => {
dispatch(toggleSettingsModal(value));
},
getPreferences: () => {
dispatch(getPreferences());
},
onToggleFlag: (name, value) => {
dispatch(changeFlag(name, value));
},
Expand All @@ -225,6 +273,9 @@ export const mapDispatchToProps = (dispatch) => ({
onToggleShowFeatureHints: (value) => {
dispatch(toggleShowFeatureHints(value));
},
onToggleShowDatasetPreviews: (value) => {
dispatch(updateUserPreferences({ showDatasetPreviews: value }));
},
});

export default connect(mapStateToProps, mapDispatchToProps)(SettingsModal);
7 changes: 7 additions & 0 deletions src/components/settings-modal/settings-modal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('SettingsModal', () => {
exportModal: expect.any(Boolean),
settingsModal: expect.any(Boolean),
}),
showDatasetPreviews: expect.any(Boolean),
flags: expect.any(Object),
isPrettyName: expect.any(Boolean),
showFeatureHints: expect.any(Boolean),
Expand Down Expand Up @@ -73,5 +74,11 @@ describe('SettingsModal', () => {
type: 'TOGGLE_IS_PRETTY_NAME',
isPrettyName: false,
});

mapDispatchToProps(dispatch).onToggleShowDatasetPreviews(false);
expect(dispatch.mock.calls[3][0]).toEqual({
type: 'UPDATE_USER_PREFERENCES',
payload: { showDatasetPreviews: false },
});
});
});
5 changes: 5 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export const settings = {
description: 'Enable or disable all new feature hints in the interface.',
default: true,
},
showDatasetPreviews: {
name: 'Dataset previews',
description: 'Display preview data for all datasets.',
default: true,
},
};

// Sidebar groups is an ordered map of { id: label }
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
TOGGLE_EXPAND_ALL_PIPELINES,
UPDATE_STATE_FROM_OPTIONS,
} from '../actions';
import userPreferences from './preferences';
import { TOGGLE_PARAMETERS_HOVERED } from '../actions';

/**
Expand Down Expand Up @@ -83,6 +84,7 @@ const combinedReducer = combineReducers({
modularPipeline,
visible,
runsMetadata,
userPreferences,
// These props don't have any actions associated with them
display: createReducer(null),
dataSource: createReducer(null),
Expand Down
19 changes: 19 additions & 0 deletions src/reducers/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UPDATE_USER_PREFERENCES } from '../actions/preferences';

const initialState = {
showDatasetPreviews: true,
};

const userPreferences = (state = initialState, action) => {
switch (action.type) {
case UPDATE_USER_PREFERENCES:
return {
...state,
showDatasetPreviews: action.payload.showDatasetPreviews,
};
default:
return state;
}
};

export default userPreferences;
14 changes: 14 additions & 0 deletions src/reducers/reducers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { UPDATE_ACTIVE_PIPELINE } from '../actions/pipelines';
import { TOGGLE_MODULAR_PIPELINE_ACTIVE } from '../actions/modular-pipelines';
import { TOGGLE_GRAPH_LOADING } from '../actions/graph';
import { UPDATE_USER_PREFERENCES } from '../actions/preferences';

describe('Reducer', () => {
it('should return an Object', () => {
Expand Down Expand Up @@ -165,6 +166,19 @@ describe('Reducer', () => {
});
});

describe('UPDATE_USER_PREFERENCES', () => {
it('should update the value of showDatasetPreviews', () => {
const newState = reducer(mockState.spaceflights, {
type: UPDATE_USER_PREFERENCES,
payload: { showDatasetPreviews: true },
});
expect(mockState.spaceflights.userPreferences.showDatasetPreviews).toBe(
true
);
expect(newState.userPreferences.showDatasetPreviews).toBe(true);
});
});

describe('TOGGLE_TEXT_LABELS', () => {
it('should toggle the value of textLabels', () => {
const newState = reducer(mockState.spaceflights, {
Expand Down
Loading

0 comments on commit 0a335b8

Please sign in to comment.