diff --git a/package-lock.json b/package-lock.json index c37a4e45ab..63cb765d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@apollo/client": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.5.tgz", - "integrity": "sha512-EiQstc8VjeqosS2h21bwY9fhL3MCRRmACtRrRh2KYpp9vkDyx5pUfMnN3swgiBVYw1twdXg9jHmyZa1gZlvlog==", + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.6.tgz", + "integrity": "sha512-XHoouuEJ4L37mtfftcHHO1caCRrKKAofAwqRoq28UQIPMJk+e7n3X9OtRRNXKk/9tmhNkwelSary+EilfPwI7A==", "requires": { "@graphql-typed-document-node/core": "^3.0.0", "@wry/context": "^0.6.0", @@ -18,7 +18,7 @@ "optimism": "^0.16.1", "prop-types": "^15.7.2", "symbol-observable": "^4.0.0", - "ts-invariant": "^0.9.0", + "ts-invariant": "^0.9.4", "tslib": "^2.3.0", "zen-observable-ts": "^1.2.0" } diff --git a/package.json b/package.json index a16ce726ef..7a594f68e5 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "snyk-test": "snyk test -prune-repeated-subdependencies" }, "dependencies": { - "@apollo/client": "^3.5.5", + "@apollo/client": "^3.5.6", "@graphql-tools/schema": "7.1.5", "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", @@ -138,4 +138,4 @@ "not op_mini all" ], "snyk": true -} \ No newline at end of file +} diff --git a/package/kedro_viz/api/graphql.py b/package/kedro_viz/api/graphql.py index 12131e056c..80029bd126 100644 --- a/package/kedro_viz/api/graphql.py +++ b/package/kedro_viz/api/graphql.py @@ -27,6 +27,7 @@ class JSONObject(dict): https://github.com/python/mypy/issues/2477 """ + else: JSONObject = strawberry.scalar( NewType("JSONObject", dict), @@ -46,7 +47,11 @@ def format_run(run_id: str, run_blob: Dict) -> Run: """ session = data_access_manager.db_session git_data = run_blob.get("git") - user_details = session.query(UserRunDetailsModel).filter(UserRunDetailsModel.run_id == run_id).scalar() + user_details = ( + session.query(UserRunDetailsModel) + .filter(UserRunDetailsModel.run_id == run_id) + .scalar() + ) run = Run( id=ID(run_id), author="", @@ -95,7 +100,9 @@ def get_all_runs() -> List[Run]: return runs -def format_run_tracking_data(tracking_data: Dict, show_diff: Optional[bool] = False) -> JSONObject: +def format_run_tracking_data( + tracking_data: Dict, show_diff: Optional[bool] = False +) -> JSONObject: """Convert tracking data in the front-end format. Args: @@ -140,7 +147,9 @@ def format_run_tracking_data(tracking_data: Dict, show_diff: Optional[bool] = Fa for run_id, run_tracking_data in tracking_data.items(): for tracking_name, data in run_tracking_data.items(): - formatted_tracking_data[tracking_name].append({"runId": run_id, "value": data}) + formatted_tracking_data[tracking_name].append( + {"runId": run_id, "value": data} + ) if not show_diff: for tracking_key, run_tracking_data in list(formatted_tracking_data.items()): if len(run_tracking_data) != len(tracking_data): @@ -149,7 +158,9 @@ def format_run_tracking_data(tracking_data: Dict, show_diff: Optional[bool] = Fa return JSONObject(formatted_tracking_data) -def get_run_tracking_data(run_ids: List[ID], show_diff: Optional[bool] = False) -> List[TrackingDataset]: +def get_run_tracking_data( + run_ids: List[ID], show_diff: Optional[bool] = False +) -> List[TrackingDataset]: # pylint: disable=protected-access,import-outside-toplevel """Get all tracking data for a list of runs. Tracking data contains the data from the tracking MetricsDataSet and JSONDataSet instances that have been logged @@ -179,7 +190,9 @@ def get_run_tracking_data(run_ids: List[ID], show_diff: Optional[bool] = False) run_id = ID(run_id) file_path = dataset._get_versioned_path(str(run_id)) if Path(file_path).is_file(): - with dataset._fs.open(file_path, **dataset._fs_open_args_load) as fs_file: + with dataset._fs.open( + file_path, **dataset._fs_open_args_load + ) as fs_file: json_data = json.load(fs_file) all_runs[run_id] = json_data else: @@ -238,7 +251,9 @@ def run_metadata(self, run_ids: List[ID]) -> List[Run]: return get_runs(run_ids) @strawberry.field - def run_tracking_data(self, run_ids: List[ID], show_diff: Optional[bool] = False) -> List[TrackingDataset]: + def run_tracking_data( + self, run_ids: List[ID], show_diff: Optional[bool] = False + ) -> List[TrackingDataset]: """Query to get data for specific runs from the session store""" return get_run_tracking_data(run_ids, show_diff) @@ -261,18 +276,20 @@ class RunInput: class UpdateRunDetailsSuccess: """Response type for sucessful update of runs""" - run_details: JSONObject + run: Run @strawberry.type class UpdateRunDetailsFailure: """Response type for failed update of runs""" - run_id: ID + id: ID error_message: str -Response = strawberry.union("UpdateRunDetailsResponse", (UpdateRunDetailsSuccess, UpdateRunDetailsFailure)) +Response = strawberry.union( + "UpdateRunDetailsResponse", (UpdateRunDetailsSuccess, UpdateRunDetailsFailure) +) @strawberry.type @@ -284,34 +301,50 @@ def update_run_details(self, run_id: ID, run_input: RunInput) -> Response: """Updates run details based on run inputs provided by user""" runs = get_runs([run_id]) if not runs: - return UpdateRunDetailsFailure(run_id=run_id, error_message=f"Given run_id: {run_id} doesn't exist") + return UpdateRunDetailsFailure( + id=run_id, error_message=f"Given run_id: {run_id} doesn't exist" + ) existing_run = runs[0] - updated_user_run_details = { - "run_id": run_id, - "bookmark": run_input.bookmark if run_input.bookmark is not None else existing_run.bookmark, - "notes": run_input.notes if run_input.notes is not None else existing_run.notes, - } - + new_run = existing_run # if user doesn't provide a new title, use the old title. if run_input.title is None: - updated_user_run_details["title"] = existing_run.title + new_run.title = existing_run.title # if user provides an empty title, we assume they want to revert to the old timestamp title elif run_input.title.strip() == "": - updated_user_run_details["title"] = existing_run.timestamp + new_run.title = existing_run.timestamp else: - updated_user_run_details["title"] = run_input.title + new_run.title = run_input.title + + new_run.bookmark = ( + run_input.bookmark + if run_input.bookmark is not None + else existing_run.bookmark + ) + + new_run.notes = ( + run_input.notes if run_input.notes is not None else existing_run.notes + ) + + updated_user_run_details = { + "run_id": run_id, + "title": new_run.title, + "bookmark": new_run.bookmark, + "notes": new_run.notes, + } session = data_access_manager.db_session - user_run_details = session.query(UserRunDetailsModel).filter(UserRunDetailsModel.run_id == run_id).first() + user_run_details = ( + session.query(UserRunDetailsModel) + .filter(UserRunDetailsModel.run_id == run_id) + .first() + ) if not user_run_details: - session.add(UserRunDetailsModel(**updated_user_run_details)) # type: ignore + session.add(UserRunDetailsModel(**updated_user_run_details)) # type: ignore else: for key, value in updated_user_run_details.items(): setattr(user_run_details, key, value) session.commit() - return UpdateRunDetailsSuccess( - run_details=JSONObject(updated_user_run_details) - ) + return UpdateRunDetailsSuccess(new_run) schema = strawberry.Schema(query=Query, mutation=Mutation) diff --git a/package/kedro_viz/integrations/kedro/sqlite_store.py b/package/kedro_viz/integrations/kedro/sqlite_store.py index a54744839d..1a39cbaa7d 100644 --- a/package/kedro_viz/integrations/kedro/sqlite_store.py +++ b/package/kedro_viz/integrations/kedro/sqlite_store.py @@ -51,7 +51,7 @@ def to_json(self) -> str: branch = git.Repo(search_parent_directories=True).active_branch value["branch"] = branch.name - except ImportError as exc: # pragma: no cover + except ImportError as exc: # pragma: no cover logger.warning("%s:%s", exc.__class__.__name__, exc.msg) if _is_json_serializable(value): diff --git a/package/tests/test_api/test_graphql.py b/package/tests/test_api/test_graphql.py index 73032d10ad..dd2ffbcd57 100644 --- a/package/tests/test_api/test_graphql.py +++ b/package/tests/test_api/test_graphql.py @@ -534,23 +534,43 @@ def test_graphql_runs_tracking_data_endpoint( class TestGraphQLMutation: - @pytest.mark.parametrize("bookmark,notes,title", [ - (False, "new notes", "new title", ), - (True, "new notes", "new title"), - (True, "", ""), - ]) + @pytest.mark.parametrize( + "bookmark,notes,title", + [ + ( + False, + "new notes", + "new title", + ), + (True, "new notes", "new title"), + (True, "", ""), + ], + ) def test_update_user_details_success( - self, bookmark, notes, title, client, save_version, example_runs, example_db_dataset, mocker + self, + bookmark, + notes, + title, + client, + save_version, + example_runs, + example_db_dataset, + mocker, ): query = f""" mutation updateRun {{ updateRunDetails(runId: "{save_version}", runInput: {{bookmark: {str(bookmark).lower()}, notes: "{notes}", title: "{title}"}}) {{ __typename ... on UpdateRunDetailsSuccess {{ - runDetails + run {{ + id + title + bookmark + notes + }} }} ... on UpdateRunDetailsFailure {{ - runId + id errorMessage }} }} @@ -563,13 +583,13 @@ def test_update_user_details_success( ) as mock_session: mock_session.return_value = example_db_dataset mocker.patch("kedro_viz.api.graphql.get_runs").return_value = example_runs - response = client.post("/graphql", json={"query": query}) + response = client.post("/graphql", json={"query": query}) assert response.json() == { "data": { "updateRunDetails": { "__typename": "UpdateRunDetailsSuccess", - "runDetails": { - "run_id": save_version, + "run": { + "id": save_version, "bookmark": bookmark, "title": title if title != "" else save_version, "notes": notes, @@ -586,10 +606,15 @@ def test_update_user_details_only_bookmark( updateRunDetails(runId: "{save_version}", runInput: {{bookmark: true}}) {{ __typename ... on UpdateRunDetailsSuccess {{ - runDetails + run {{ + id + title + bookmark + notes + }} }} ... on UpdateRunDetailsFailure {{ - runId + id errorMessage }} }} @@ -602,13 +627,13 @@ def test_update_user_details_only_bookmark( ) as mock_session: mock_session.return_value = example_db_dataset mocker.patch("kedro_viz.api.graphql.get_runs").return_value = example_runs - response = client.post("/graphql", json={"query": query}) + response = client.post("/graphql", json={"query": query}) assert response.json() == { "data": { "updateRunDetails": { "__typename": "UpdateRunDetailsSuccess", - "runDetails": { - "run_id": save_version, + "run": { + "id": save_version, "bookmark": True, "title": example_runs[0].title, "notes": example_runs[0].notes, @@ -617,16 +642,23 @@ def test_update_user_details_only_bookmark( } } - def test_update_user_details_should_add_when_it_does_not_exist(self, save_version, client, example_runs, mocker): + def test_update_user_details_should_add_when_it_does_not_exist( + self, save_version, client, example_runs, mocker + ): query = f""" mutation updateRun {{ updateRunDetails(runId: "{save_version}", runInput: {{bookmark: true}}) {{ __typename ... on UpdateRunDetailsSuccess {{ - runDetails + run {{ + id + title + bookmark + notes + }} }} ... on UpdateRunDetailsFailure {{ - runId + id errorMessage }} }} @@ -637,15 +669,17 @@ def test_update_user_details_should_add_when_it_does_not_exist(self, save_versio "kedro_viz.data_access.DataAccessManager.db_session", new_callable=PropertyMock, ) as mock_session: - mock_session.return_value.query.return_value.filter.return_value.first.return_value = None + mock_session.return_value.query.return_value.filter.return_value.first.return_value = ( + None + ) mocker.patch("kedro_viz.api.graphql.get_runs").return_value = example_runs - response = client.post("/graphql", json={"query": query}) + response = client.post("/graphql", json={"query": query}) assert response.json() == { "data": { "updateRunDetails": { "__typename": "UpdateRunDetailsSuccess", - "runDetails": { - "run_id": save_version, + "run": { + "id": save_version, "bookmark": True, "title": example_runs[0].title, "notes": example_runs[0].notes, @@ -673,10 +707,15 @@ def test_update_user_details_fail(self, client, example_db_dataset, mocker): { __typename ... on UpdateRunDetailsSuccess { - runDetails + run { + id + title + notes + bookmark + } } ... on UpdateRunDetailsFailure { - runId + id errorMessage } } }""" @@ -686,7 +725,7 @@ def test_update_user_details_fail(self, client, example_db_dataset, mocker): "data": { "updateRunDetails": { "__typename": "UpdateRunDetailsFailure", - "runId": "2021-11-02T12.24.24.329Z", + "id": "2021-11-02T12.24.24.329Z", "errorMessage": "Given run_id: 2021-11-02T12.24.24.329Z doesn't exist", } } diff --git a/src/apollo/config.js b/src/apollo/config.js index c3c6927302..90a94c4e32 100644 --- a/src/apollo/config.js +++ b/src/apollo/config.js @@ -11,10 +11,12 @@ export const client = new ApolloClient({ connectToDevTools: true, link, cache: new InMemoryCache(), - resolvers: {}, defaultOptions: { query: { errorPolicy: 'all', }, + mutate: { + errorPolicy: 'all', + }, }, }); diff --git a/src/apollo/mutations.js b/src/apollo/mutations.js new file mode 100644 index 0000000000..c25e4d145c --- /dev/null +++ b/src/apollo/mutations.js @@ -0,0 +1,20 @@ +import gql from 'graphql-tag'; + +export const UPDATE_RUN_DETAILS = gql` + mutation updateRunDetails($runId: ID!, $runInput: RunInput!) { + updateRunDetails(runId: $runId, runInput: $runInput) { + ... on UpdateRunDetailsFailure { + errorMessage + id + } + ... on UpdateRunDetailsSuccess { + run { + bookmark + id + notes + title + } + } + } + } +`; diff --git a/src/apollo/queries.js b/src/apollo/queries.js index 6527eab7b3..a103bce3c3 100644 --- a/src/apollo/queries.js +++ b/src/apollo/queries.js @@ -21,6 +21,7 @@ export const GET_RUN_METADATA = gql` author gitBranch gitSha + id notes runCommand timestamp diff --git a/src/apollo/schema.graphql b/src/apollo/schema.graphql index 9a32bd8b4f..9397a7c916 100644 --- a/src/apollo/schema.graphql +++ b/src/apollo/schema.graphql @@ -36,12 +36,12 @@ type TrackingDataset { } type UpdateRunDetailsFailure { - runId: ID! + id: ID! errorMessage: String! } union UpdateRunDetailsResponse = UpdateRunDetailsSuccess | UpdateRunDetailsFailure type UpdateRunDetailsSuccess { - runDetails: JSONObject! + run: Run! } diff --git a/src/apollo/schema.js b/src/apollo/schema.js index c82f71f952..0eb7e3e31d 100644 --- a/src/apollo/schema.js +++ b/src/apollo/schema.js @@ -51,7 +51,7 @@ const typeDefs = gql` } type UpdateRunDetailsFailure { - runId: ID! + id: ID! errorMessage: String! } @@ -60,7 +60,7 @@ const typeDefs = gql` | UpdateRunDetailsFailure type UpdateUserDetailsSuccess { - userDetails: JSONObject! + run: Run! } `; diff --git a/src/components/experiment-tracking/details/index.js b/src/components/experiment-tracking/details/index.js index 7276d269be..d68143dd33 100644 --- a/src/components/experiment-tracking/details/index.js +++ b/src/components/experiment-tracking/details/index.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useApolloQuery } from '../../../apollo/utils'; import classnames from 'classnames'; import RunMetadata from '../run-metadata'; import RunDataset from '../run-dataset'; +import RunDetailsModal from '../run-details-modal'; import { GET_RUN_METADATA, GET_RUN_TRACKING_DATA, @@ -11,12 +12,17 @@ import { import './details.css'; const Details = ({ + enableComparisonView, enableShowChanges, pinnedRun, selectedRuns, setPinnedRun, + setShowRunDetailsModal, + showRunDetailsModal, sidebarVisible, + theme, }) => { + const [runMetadataToEdit, setRunMetadataToEdit] = useState(null); const { data: { runMetadata } = [], error } = useApolloQuery( GET_RUN_METADATA, { @@ -31,14 +37,29 @@ const Details = ({ variables: { runIds: selectedRuns, showDiff: false }, }); + useEffect(() => { + if (runMetadata && !enableComparisonView) { + const metadata = runMetadata.find((run) => run.id === selectedRuns[0]); + + setRunMetadataToEdit(metadata); + } + }, [enableComparisonView, runMetadata, selectedRuns]); + + const isSingleRun = runMetadata?.length === 1 ? true : false; + if (error || trackingError) { return null; } - const isSingleRun = runMetadata && runMetadata.length === 1 ? true : false; - return ( <> +
{ return ( + showRunDetailsModal(true)} + visible={!enableComparisonView} + /> { + const [valuesToUpdate, setValuesToUpdate] = useState({}); + const [updateRunDetails, { error, reset }] = useMutation(UPDATE_RUN_DETAILS, { + client, + }); + + const onApplyChanges = () => { + updateRunDetails({ + variables: { + runId: runMetadataToEdit.id, + runInput: { notes: valuesToUpdate.notes, title: valuesToUpdate.title }, + }, + }); + }; + + const onChange = (key, value) => { + setValuesToUpdate( + Object.assign({}, valuesToUpdate, { + [key]: value, + }) + ); + }; + + useEffect(() => { + setValuesToUpdate({ + notes: runMetadataToEdit?.notes, + title: runMetadataToEdit?.title, + }); + }, [runMetadataToEdit]); + + useEffect(() => { + /** + * If there's a GraphQL error when trying to update the title/notes, + * reset the mutation when the modal closes so the error doesn't appear + * the next time the modal opens. + */ + reset(); + }, [reset, visible]); + + return ( +
+ onClose(false)} + theme={theme} + title="Edit run details" + visible={visible} + > +
+
+
Run name
+
+ onChange('title', value)} + size="large" + /> +
+
+
+
Notes
+
+ onChange('notes', value)} + placeholder="Add here" + resetValueTrigger={visible} + size="small" + /> +
+
+ + +
+ {error ? ( +
+

Couldn't update run details. Please try again later.

+
+ ) : null} +
+
+ ); +}; + +export default RunDetailsModal; diff --git a/src/components/experiment-tracking/run-details-modal/run-details-modal.scss b/src/components/experiment-tracking/run-details-modal/run-details-modal.scss new file mode 100644 index 0000000000..baab3095cc --- /dev/null +++ b/src/components/experiment-tracking/run-details-modal/run-details-modal.scss @@ -0,0 +1,20 @@ +.pipeline-settings-modal--experiment-tracking .pipeline-settings-modal__name { + margin-top: 0; +} + +.run-details-modal-button-wrapper { + align-items: baseline; + display: flex; + justify-content: flex-end; + width: 100%; + + .kui-button:first-of-type { + margin-right: 20px; + } +} + +.run-details-modal-error-wrapper { + font-size: 15px; + text-align: right; + width: 100%; +} diff --git a/src/components/experiment-tracking/run-details-modal/run-details-modal.test.js b/src/components/experiment-tracking/run-details-modal/run-details-modal.test.js new file mode 100644 index 0000000000..a5bde71902 --- /dev/null +++ b/src/components/experiment-tracking/run-details-modal/run-details-modal.test.js @@ -0,0 +1,33 @@ +import React from 'react'; +import RunDetailsModal from './index'; +import Adapter from 'enzyme-adapter-react-16'; +import { configure, mount, shallow } from 'enzyme'; + +configure({ adapter: new Adapter() }); + +describe('RunDetailsModal', () => { + it('renders without crashing', () => { + const wrapper = shallow(); + + expect( + wrapper.find('.pipeline-settings-modal--experiment-tracking').length + ).toBe(1); + }); + + it('modal closes when X button is clicked', () => { + const setVisible = jest.fn(); + const wrapper = mount( setVisible(true)} />); + const onClick = jest.spyOn(React, 'useState'); + + onClick.mockImplementation((visible) => [visible, setVisible]); + const closeButton = wrapper.find( + '.pipeline-settings-modal--experiment-tracking .modal__close-button.pipeline-icon-toolbar__button' + ); + closeButton.simulate('click'); + expect( + wrapper.find( + '.pipeline-settings-modal--experiment-tracking .kui-modal--visible' + ).length + ).toBe(0); + }); +}); diff --git a/src/components/experiment-tracking/run-metadata/index.js b/src/components/experiment-tracking/run-metadata/index.js index 6ed4e9fd28..a4b4bcfa54 100644 --- a/src/components/experiment-tracking/run-metadata/index.js +++ b/src/components/experiment-tracking/run-metadata/index.js @@ -7,16 +7,19 @@ import { toHumanReadableTime } from '../../../utils/date-utils'; import './run-metadata.css'; -// We are only checking for an empty string as it is the default value -// returned by the graphql endpoint for empty values ( not null or undefined ) -const sanitiseEmptyValue = (value) => (value !== '' ? value : '-'); +// Return a '-' character if the value is empty or null +const sanitiseEmptyValue = (value) => { + return value === '' || value === null ? '-' : value; +}; const RunMetadata = ({ - isSingleRun, - runs = [], enableShowChanges = false, + isSingleRun, pinnedRun, + runs = [], setPinnedRun, + setRunMetadataToEdit, + setShowRunDetailsModal, }) => { let initialState = {}; for (let i = 0; i < runs.length; i++) { @@ -29,6 +32,13 @@ const RunMetadata = ({ setToggleNotes({ ...toggleNotes, [index]: !toggleNotes[index] }); }; + const onTitleOrNoteClick = (id) => { + const metadata = runs.find((run) => run.id === id); + + setRunMetadataToEdit(metadata); + setShowRunDetailsModal(true); + }; + return (
- {sanitiseEmptyValue(run.title)} + onTitleOrNoteClick(run.id)}> + {sanitiseEmptyValue(run.title)} + ) : ( {i === 0 ? : null} - {sanitiseEmptyValue(run.title)} -
    + onTitleOrNoteClick(run.id)}> + {sanitiseEmptyValue(run.title)} + +

      onTitleOrNoteClick(run.id)} style={toggleNotes[i] ? { display: 'block' } : null} > - {sanitiseEmptyValue(run.notes)} + {run.notes !== '' ? run.notes : '- Add notes here'}

      {run.notes.length > 100 ? ( +
)} diff --git a/src/components/icons/pencil.js b/src/components/icons/pencil.js new file mode 100644 index 0000000000..a5f808e371 --- /dev/null +++ b/src/components/icons/pencil.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const PencilIcon = ({ className }) => ( + + + +); + +export default PencilIcon; diff --git a/src/components/primary-toolbar/index.js b/src/components/primary-toolbar/index.js index cd1bc5a91c..3e8fee31d2 100644 --- a/src/components/primary-toolbar/index.js +++ b/src/components/primary-toolbar/index.js @@ -2,18 +2,19 @@ import React from 'react'; import classnames from 'classnames'; import IconButton from '../icon-button'; import MenuIcon from '../icons/menu'; + import './primary-toolbar.css'; /** * Toolbar to house buttons that controls display options for the main panel (flowchart, experiment details, etc) + * @param {JSX} children The content to be rendered within the toolbar * @param {Function} onToggleSidebar Handle toggling of sidebar collapsable view * @param {Boolean} visible Handle display of tooltip text in relation to collapsable view - * @param {JSX} children The content to be rendered within the toolbar */ export const PrimaryToolbar = ({ + children, onToggleSidebar, visible = { sidebar: true }, - children, }) => ( <>
    diff --git a/src/components/provider/provider.js b/src/components/provider/provider.js index e4eb070c22..9a9a106e3a 100644 --- a/src/components/provider/provider.js +++ b/src/components/provider/provider.js @@ -8,7 +8,7 @@ import { runTrackingDataMock, } from '../../apollo/mocks'; -export const Provider = ({ useMocks, children }) => { +export const Provider = ({ useMocks = false, children }) => { if (useMocks) { return ( { ); } + return ( <>{children} diff --git a/src/components/settings-modal/settings-modal.scss b/src/components/settings-modal/settings-modal.scss index 43eb89edfc..c11c4e2f7f 100644 --- a/src/components/settings-modal/settings-modal.scss +++ b/src/components/settings-modal/settings-modal.scss @@ -39,6 +39,10 @@ margin-bottom: 4em; font-size: 1.25em; + &--short { + margin-bottom: 2.4em; + } + .kui-button { margin: 0 10px; } diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index 4cb13e0efe..49860a320a 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -1,15 +1,14 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; import classnames from 'classnames'; +import ExperimentPrimaryToolbar from '../experiment-tracking/experiment-primary-toolbar'; +import FlowchartPrimaryToolbar from '../flowchart-primary-toolbar'; import MiniMap from '../minimap'; import MiniMapToolbar from '../minimap-toolbar'; import NodeList from '../node-list'; import PipelineList from '../pipeline-list'; -import FlowchartPrimaryToolbar from '../flowchart-primary-toolbar'; -import ExperimentPrimaryToolbar from '../experiment-tracking/experiment-primary-toolbar'; -import Switch from '../switch'; - import RunsList from '../experiment-tracking/runs-list'; +import Switch from '../switch'; import './sidebar.css'; @@ -20,16 +19,17 @@ import './sidebar.css'; export const Sidebar = ({ disableRunSelection, enableComparisonView, + enableShowChanges, isExperimentView = false, onRunSelection, onToggleComparisonView, runsListData, selectedRuns, - visible, - sidebarVisible, - setSidebarVisible, - enableShowChanges, setEnableShowChanges, + setSidebarVisible, + showRunDetailsModal, + sidebarVisible, + visible, }) => { const [pipelineIsOpen, togglePipeline] = useState(false); @@ -58,12 +58,13 @@ export const Sidebar = ({
diff --git a/src/components/ui/input/index.js b/src/components/ui/input/index.js new file mode 100644 index 0000000000..1ceb5f5b50 --- /dev/null +++ b/src/components/ui/input/index.js @@ -0,0 +1,70 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; + +import './input.css'; + +const MIN_HEIGHT = 20; + +const Input = ({ + characterLimit = false, + defaultValue = '', + onChange, + placeholder, + resetValueTrigger, + size = 'large', +}) => { + const isLimitSet = characterLimit > 0; + const ref = useRef(null); + const [value, setValue] = useState(defaultValue); + + useEffect(() => { + setValue(defaultValue); + }, [defaultValue]); + + useEffect(() => { + setValue(defaultValue); + }, [defaultValue, resetValueTrigger]); + + useLayoutEffect(() => { + ref.current.style.height = 'inherit'; + + ref.current.style.height = `${Math.max( + ref.current.scrollHeight, + MIN_HEIGHT + )}px`; + }, [value]); + + const handleChange = (e) => { + const value = e.target.value; + + if (isLimitSet) { + setValue(value.slice(0, characterLimit)); + onChange && onChange(value.slice(0, characterLimit)); + } else { + setValue(value.slice(0)); + onChange && onChange(value.slice(0)); + } + }; + + return ( + <> +