diff --git a/server/handler/features.go b/server/handler/features.go index aceb1d8..e1b2027 100644 --- a/server/handler/features.go +++ b/server/handler/features.go @@ -3,24 +3,51 @@ package handler import ( "net/http" - "github.com/go-chi/chi" + "github.com/src-d/code-annotation/server/model" "github.com/src-d/code-annotation/server/repository" "github.com/src-d/code-annotation/server/serializer" ) // GetFeatures returns a function that returns a *serializer.Response // with the list of features for blobId -func GetFeatures(repo *repository.Features) RequestProcessFunc { +func GetFeatures(filePairRepo *repository.FilePairs, featuresRepo *repository.Features) RequestProcessFunc { return func(r *http.Request) (*serializer.Response, error) { - blobID := chi.URLParam(r, "blobId") + filePairID, err := urlParamInt(r, "pairId") + if err != nil { + return nil, err + } + + filePair, err := filePairRepo.GetByID(filePairID) + if err != nil { + return nil, err + } - // in the future it should take file by blobID from DB - // and make API request to ML system - features, err := repo.GetAll(blobID) + featuresA, featuresB, score, err := getFeatures(featuresRepo, filePair) if err != nil { return nil, err } - return serializer.NewFeaturesResponse(features), nil + return serializer.NewFeaturesResponse(featuresA, featuresB, score), nil } } + +// TODO (dpordomingo): in the future it should take the UAST of both blobs DB +// and make a request to ML feature extractor API +func getFeatures(repo *repository.Features, pair *model.FilePair) ([]*model.Feature, []*model.Feature, *model.Feature, error) { + blobIDA := pair.Left.BlobID + blobIDB := pair.Right.BlobID + + featuresA, err := repo.GetAll(blobIDA) + if err != nil { + return nil, nil, nil, err + } + + featuresB, err := repo.GetAll(blobIDB) + if err != nil { + return nil, nil, nil, err + } + + score := model.Feature{Name: "score", Weight: pair.Score} + + return featuresA, featuresB, &score, err +} diff --git a/server/router.go b/server/router.go index d3ff6d2..5600fb6 100644 --- a/server/router.go +++ b/server/router.go @@ -89,10 +89,10 @@ func Router( r.Get("/file-pairs/{pairId}", handler.APIHandlerFunc(handler.GetFilePairDetails(filePairRepo, diffService))) }) - r.Route("/features", func(r chi.Router) { + r.Route("/file-pair", func(r chi.Router) { r.Use(requesterACL.Middleware) - r.Get("/{blobId}", handler.APIHandlerFunc(handler.GetFeatures(featureRepo))) + r.Get("/{pairId}/features", handler.APIHandlerFunc(handler.GetFeatures(filePairRepo, featureRepo))) }) r.Route("/exports", func(r chi.Router) { diff --git a/server/serializer/serializers.go b/server/serializer/serializers.go index a400750..a9b456d 100644 --- a/server/serializer/serializers.go +++ b/server/serializer/serializers.go @@ -192,13 +192,29 @@ type featureResponse struct { Weight float64 `json:"weight"` } -// NewFeaturesResponse returns a Response for the passed Features -func NewFeaturesResponse(fs []*model.Feature) *Response { - features := make([]featureResponse, len(fs)) - for i, f := range fs { - features[i] = featureResponse(*f) +type featuresResponse struct { + Object1 []featureResponse `json:"featuresA"` + Object2 []featureResponse `json:"featuresB"` + Pair featureResponse `json:"score"` +} + +// NewFeaturesResponse returns a Response for the passed Features and score +func NewFeaturesResponse(fsA []*model.Feature, fsB []*model.Feature, s *model.Feature) *Response { + featuresA := make([]featureResponse, len(fsA)) + for i, f := range fsA { + featuresA[i] = featureResponse(*f) + } + + featuresB := make([]featureResponse, len(fsB)) + for i, f := range fsB { + featuresB[i] = featureResponse(*f) } - return newResponse(features) + + return newResponse(featuresResponse{ + Object1: featuresA, + Object2: featuresB, + Pair: featureResponse(*s), + }) } type countResponse struct { diff --git a/src/api/index.js b/src/api/index.js index 2e5aefe..4c17160 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -158,8 +158,8 @@ function getFilePairAnnotations(experimentId, id) { ); } -function getFeatures(blobId) { - return apiCall(`/api/features/${blobId}`); +function getFeatures(filePairId) { + return apiCall(`/api/file-pair/${filePairId}/features`); } function exportList() { diff --git a/src/pages/Review.js b/src/pages/Review.js index 2cb7916..dbc5b1e 100644 --- a/src/pages/Review.js +++ b/src/pages/Review.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Grid, Row, Col } from 'react-bootstrap'; +import { push } from 'redux-little-router'; import SplitPane from 'react-split-pane'; import Page from './Page'; import Loader from '../components/Loader'; @@ -8,12 +9,14 @@ import Breadcrumbs from '../components/Breadcrumbs'; import Selector from '../components/Experiment/Selector'; import Diff from '../components/Experiment/Diff'; import Results from '../components/Review/Results'; +import { makeUrl } from '../state/routes'; +import { getCurrentFilePair, loadFilePair } from '../state/filePairs'; import { - getCurrentFilePair, - selectPair, - loadFilePair, -} from '../state/filePairs'; -import { getFeatures, mostSimilar, leastSimilar } from '../state/features'; + getFeatures, + mostSimilar, + leastSimilar, + getScore, +} from '../state/features'; import { toggleInvisible } from '../state/user'; import './Review.less'; @@ -64,6 +67,7 @@ class Review extends Component { mostSimilarFeatures, leastSimilarFeatures, features, + score, } = this.props; if (fileLoading || !filePair) { @@ -102,7 +106,7 @@ class Review extends Component { /> { name: `(${i + 1})`, })); + const score = getScore(state); const features = getFeatures(state); // keep only 100 results // it will be improved (most probably with pagination) later @@ -146,13 +151,17 @@ const mapStateToProps = state => { mostSimilarFeatures, leastSimilarFeatures, features, + score, showInvisible, }; }; const experimentId = 1; const mapDispatchToProps = dispatch => ({ - onSelect: pairId => dispatch(selectPair(experimentId, pairId)), + onSelect: pairId => + dispatch( + push(makeUrl('reviewPair', { experiment: experimentId, pair: pairId })) + ), toggleInvisible: (expId, id) => { dispatch(toggleInvisible()); return dispatch(loadFilePair(expId, id)); diff --git a/src/state/features.js b/src/state/features.js index 46469de..415ed2b 100644 --- a/src/state/features.js +++ b/src/state/features.js @@ -33,11 +33,14 @@ const reducer = (state = initialState, action) => { // Actions -export const load = (blobIdA, blobIdB) => dispatch => { +export const load = filePairId => dispatch => { dispatch({ type: LOAD }); - return Promise.all([api.getFeatures(blobIdA), api.getFeatures(blobIdB)]) - .then(([featuresA, featuresB]) => { + return api + .getFeatures(filePairId) + .then(res => { + const { featuresA, featuresB, score } = res; + // collect names from both responses const names = featuresA.map(f => f.name); featuresB.forEach(f => { @@ -57,12 +60,17 @@ export const load = (blobIdA, blobIdB) => dispatch => { }, {}); // merge features by name - const features = names.map(name => ({ + const individualFeatures = names.map(name => ({ name, weightA: mapA[name] || 0, weightB: mapB[name] || 0, })); + const features = { + features: individualFeatures, + score: score.weight, + }; + return dispatch({ type: LOAD_SUCCESS, features }); }) .catch(err => { @@ -72,7 +80,11 @@ export const load = (blobIdA, blobIdB) => dispatch => { // Selectors -export const getFeatures = state => state.features.features; +export const getFeatures = state => + (state.features.features && state.features.features.features) || []; + +export const getScore = state => + (state.features.features && state.features.features.score) || 0; export const mostSimilar = createSelector(getFeatures, features => { // copy because sort mutates array diff --git a/src/state/features.test.js b/src/state/features.test.js index 5a48e85..ca3ae67 100644 --- a/src/state/features.test.js +++ b/src/state/features.test.js @@ -27,47 +27,44 @@ describe('features/reducer', () => { describe('features/actions', () => { describe('load', () => { it('success', () => { - const blobIdA = 1; - const blobIdB = 2; + const filePairId = 1; const store = mockStore({ features: { ...initialState, }, }); - fetch.mockResponses( - // blobA response - [ - JSON.stringify({ - data: [ + fetch.mockResponses([ + JSON.stringify({ + data: { + featuresA: [ { name: 'feature1', weight: 0.9 }, { name: 'feature2', weight: 0.8 }, ], - }), - ], - // blobB response - [ - JSON.stringify({ - data: [ + featuresB: [ { name: 'feature1', weight: 0.8 }, { name: 'feature3', weight: 0.1 }, ], - }), - ] - ); + score: { name: 'score', weight: 0.5 }, + }, + }), + ]); - return store.dispatch(load(blobIdA, blobIdB)).then(() => { + return store.dispatch(load(filePairId)).then(() => { expect(store.getActions()).toEqual([ { type: LOAD, }, { type: LOAD_SUCCESS, - features: [ - { name: 'feature1', weightA: 0.9, weightB: 0.8 }, - { name: 'feature2', weightA: 0.8, weightB: 0 }, - { name: 'feature3', weightA: 0, weightB: 0.1 }, - ], + features: { + features: [ + { name: 'feature1', weightA: 0.9, weightB: 0.8 }, + { name: 'feature2', weightA: 0.8, weightB: 0 }, + { name: 'feature3', weightA: 0, weightB: 0.1 }, + ], + score: 0.5, + }, }, ]); }); @@ -103,13 +100,16 @@ describe('features/actions', () => { describe('features/selectors', () => { const stateForSort = { features: { - features: [ - { name: 'a', weightA: 5, weightB: 10 }, - { name: 'b', weightA: 10, weightB: 10 }, - { name: 'c', weightA: 10, weightB: 1 }, - { name: 'b', weightA: 2, weightB: 2 }, - { name: 'd', weightA: 10, weightB: 10 }, - ], + features: { + features: [ + { name: 'a', weightA: 5, weightB: 10 }, + { name: 'b', weightA: 10, weightB: 10 }, + { name: 'c', weightA: 10, weightB: 1 }, + { name: 'b', weightA: 2, weightB: 2 }, + { name: 'd', weightA: 10, weightB: 10 }, + ], + score: 0.5, + }, }, }; diff --git a/src/state/filePairs.js b/src/state/filePairs.js index 5b7ab6e..fb0d1ae 100644 --- a/src/state/filePairs.js +++ b/src/state/filePairs.js @@ -192,7 +192,7 @@ export const middleware = store => next => action => { .then(() => next(loadAnnotations(expIdParam, +payload.params.pair))) .then(() => { const pair = getCurrentFilePair(store.getState()); - return pair && next(featuresLoad(pair.leftBlobId, pair.rightBlobId)); + return pair && next(featuresLoad(pair.id)); }); default: return result; diff --git a/src/state/routes.test.js b/src/state/routes.test.js index 6a41406..a08acca 100644 --- a/src/state/routes.test.js +++ b/src/state/routes.test.js @@ -288,17 +288,14 @@ describe('routers', () => { }), { status: 200 }, ], - // features left + // features [ JSON.stringify({ - data: [], - }), - { status: 200 }, - ], - // features right - [ - JSON.stringify({ - data: [], + data: { + featuresA: [], + featuresB: [], + score: {}, + }, }), { status: 200 }, ]