Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph visualization - initial code #451

Merged
merged 2 commits into from
Oct 17, 2017
Merged

Conversation

franekp
Copy link
Contributor

@franekp franekp commented Oct 10, 2017

No description provided.

@berggren berggren requested a review from We-St October 10, 2017 14:02
@@ -0,0 +1,42 @@
/*
Copyright 2015 Google Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/2015/2017

Copy link

@We-St We-St left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First half of the review. I'll continue later.

import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http'

export class AttachCsrfTokenInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, httpHandler: HttpHandler): Observable<HttpEvent<any>> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do not rely on the type but need to provide one anyway, use "{}" rather "any". "any" deactivates the type checking completely, while "{}" is the empty type.

@@ -0,0 +1,12 @@
import {Observable} from 'rxjs'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file here (and most others I have seen so far), rely on JavaScript's automatic semicolon insertion. This is quite dangerous and I would recommend moving away from it. There are some nasty edge-cases (e.g. around the return statement).


export class AttachCsrfTokenInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, httpHandler: HttpHandler): Observable<HttpEvent<any>> {
const csrfToken = document.getElementsByTagName('meta')[0]['content']
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document.getElementsByTagName('meta')[0] is quite fragile: if anyone inserts a new meta tag, requests will start failing.

What about giving the meta tag a pseudo-attribute (e.g. "xsrf-token") and use document.querySelector('[xsrf-token]') to get it? Reference: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

})
})

it('.search() should return a proper response for a query', async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it() typically forms sentence, e.g. it('should do something'). The it() refers to the describe block. The output in case of error will be: Graphservice should do something.

You can have nested describe blocks (one for 'GraphService', one for 'search()') if you want or you can rename the first param of it.


@Injectable()
export class GraphService {
constructor(private sketchService: SketchService, private http: HttpClient) {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: it's good practice to make all injected services "readonly" in addition to being private. However, no pressing need for this.

it('should emit the "cypherSearch" event when user hits enter key', async () => {
const textbox: HTMLInputElement = fixture.debugElement.query(By.css('input[type="search"]')).nativeElement
const form: HTMLFormElement = fixture.debugElement.query(By.css('form')).nativeElement
const cypherSearchPromise = new Promise((resolve) => fixture.componentInstance.cypherSearch.subscribe(resolve))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toPromise(). I'll leave it up to you to change it everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, here it has a chance to work.

textbox.value = 'MATCH (a) RETURN (a)'
textbox.dispatchEvent(new Event('input'))
fixture.detectChanges()
form.dispatchEvent(new Event('submit'))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't test the enter key as the description says.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will change the description. Testing enter key is not quite possible with PhantomJS.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not simply do:

const event = new KeyboardEvent('keyup', {'key': 'Enter'});
someElement.dispatchEvent(event);

?

Copy link
Contributor Author

@franekp franekp Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PhantomJS says that KeyboardEventConstructor is not a constructor, I would rather not put time into investigating this.

expect(cypherSearch).toEqual('MATCH (a) RETURN (a)')
})

it('should emit the "cypherSearch" event when user presses search button', async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: this is nearly a duplicate of the previous test. You can nest this in another describe() block and have another beforeEach() to unify test setup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
</style>
<!-- interaction options -->
<label>minZoom = <input [(ngModel)]="settings.minZoom" type="number" /></label><br />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this a list (ul and li) rather than use brs? You'd have more styling flexibility that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


@Component({
template: `
<ts-graphs-cytoscape-settings [settings]="settings">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close tag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

@franekp franekp force-pushed the graphs-initial branch 3 times, most recently from 87b40d4 to 7d777ea Compare October 11, 2017 17:31
@franekp
Copy link
Contributor Author

franekp commented Oct 11, 2017

@berggren Ready to merge.
EDIT: just noticed this was just first half of the review. Let's wait for Stefan to finish second half.

Copy link

@We-St We-St left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments.

@@ -0,0 +1,91 @@
type DateTime = string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this file reflects server-side resources, please add a link to that documentation to this file.

Copy link
Contributor Author

@franekp franekp Oct 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. You can check if the format of the comment is acceptable.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

const fakeSketchId = 42

sketchService.sketchId = fakeSketchId
const sketchPromise = sketchService.getSketch().toPromise()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could restructure this code a bit. What do you think about:

http.expectOne('/api/v1/sketches/42/').flush({objects: ['FAKE_SKETCH']})

sketchService.sketchId = 42
const sketch = await sketchService.getSketch().toPromise()
expect(sketch).toEqual('FAKE_SKETCH')
http.verify()

And (optionally) remove fakeResult and fakeSketchId.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -623,3 +623,65 @@ ul.content-list>li:last-child { border-bottom: none; }
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url('/static/fonts/Roboto/Roboto-Regular.ttf') format('truetype');
}

ts-graphs-cytoscape div {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should think about moving CSS to SCSS at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. But later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

)
const label: HTMLLabelElement = labelDebugElement.nativeElement
const input: HTMLInputElement = labelDebugElement.query(By.css('input')).nativeElement
if (typeof value === 'boolean') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to split this test into several, based on this if. E.g. one test for all boolean values, one for numbers, one for additive plus one to check that all fields were checked.

You could also move headless and styleEnabled into a separate one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -0,0 +1,37 @@
import {Component, Input} from '@angular/core'

export type CytoscapeSettings = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these server-side resources again? I'd recommend either adding a link to the docs or jsDoc here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, these are not server-side resources. I can add a link to Cytoscape documentation.

[style]="style"
[layout]="layout"

[minZoom]="settings.minZoom"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point, it might make sense to pass in a whole settings object rather than all sections individually.

Copy link
Contributor Author

@franekp franekp Oct 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the change detection will not detect changes to individual fields.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple ways to do this. Either you can use immtuables for this or override ngDoCheck (https://angular.io/guide/lifecycle-hooks#lifecycle-sequence). It's also ok to leave as-is for now, since this is a bigger change.

wheelSensitivity: 1,
pixelRatio: ('auto' as any),
}
layout = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an interface for layout as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one in type definitions of Cytoscape library.

Copy link
Contributor Author

@franekp franekp Oct 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add it here, you are right.

minTemp: 1.0,
useMultitasking: true,
}
style = [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be moved to CSS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is Cytoscape-specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add type annotation for that too.

@@ -0,0 +1,6 @@
<div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular2+ components do not need single template root components, so you can remove this div (if it doesn't serve any other purpose).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a flexbox container with fixed height, so it has a purpose. But this is good to know.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I wonder why not make the component element itself the flexbox container?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


import {CytoscapeSettings} from './cytoscape-settings.component'

export type GraphViewState = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't just be easier to write

export interface GraphViewState {
type: 'empty'|'loading'|'ready';
element?: ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this will no longer be semantically correct, right? I imagine later on this could become something like:

type GraphViewState = {
    type: 'empty' | 'loading' | 'ready' | 'error',
    spinnerUrl?: string,
    message?: string,
    elements?: Cy.ElementsDefinition,
    elementTypes?: ElementType[],
}

How do you figure out which fields are available for which type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would not be semantically identical to what is written now. It's ok to keep the discriminator columns and be more precise on types, but in such a scenario, I would split this single type definition into multiple definitions, e.g.

export type Empty = { ... };
export type Loading = {...};
export type GraphViewState = Empty | Loading | ... ;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@franekp franekp force-pushed the graphs-initial branch 16 times, most recently from 586e1da to 905084a Compare October 16, 2017 14:42
@@ -46,8 +44,8 @@ export const tsCoreFileModel = ['$parse', function ($parse) {
const model = $parse(attrs.tsCoreFileModel)
const modelSetter = model.assign

element.bind('change', function (){
scope.$apply(function (){
element.bind('change', function () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be changed to arrow functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather create a separate PR which changes it everywhere.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

div {
@include ts-graphs-fill-space
}
@include ts-graphs-fill-space
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolons here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@Output() cypherSearch = new EventEmitter<string>()
query = 'MATCH (a)-[e]->(b) RETURN *'

onSubmit(event: Event) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event is unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

})

it('should have tests covering all cytoscape options', () => {
expect(Object.keys(remainingOptions).length).toEqual(0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes your tests depend on each other and easier to break. If someone adds another test or moves them around, they will start to fail. I proposed a different solution in a comment further up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this makes them order-dependent, but they are guaranted to execute in order they are defined. Solution from your comment is less dry, so it is not clear which one is better.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having your tests depend on each other is probably a lot worse than minimal repetition. Especially because you guarantee with your test that the duplicated information stays in sync. (Note that Jasmine is not the problem here. It guarantees execution order, but you cannot be sure that new code will not be added after your test).

If you choose to leave it like this, please add a striking comment to the last test, something like:

// -------------------- THIS TEST NEEDS TO BE LAST --------------------
// This test verifies that all options were properly represented in the
// templates. Due to inter-dependent tests, this NEEDS to go last.

})

describe('checkboxes', () => {
it('should work correctly', () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create a local list of all options here that are represented by a checkbox. Then you can iterate this list and see that all options can be toggled on the UI.

To make sure that people do not forget to update the tests when new options are introduced, I would create a second test case that checks that all checkboxes are in your local list. I.e. two tests:

before(('...') => {
const booleanOptions = ['a', 'b', 'c'];
it('allows toggling options', () => {
// Checks that all elements in booleanOptions are present in the template.
});
if('does not specify additional boolean options', () => {
// Checks that all checkboxes in the template are present in booleanOptions.
});
});

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already assertions to ensure that all options are tested. Applying this suggestion would introduce some duplication, maybe even worth adding for readability. But I would rather not spend more time on it, there are other priorities.

@@ -0,0 +1,57 @@
<div *ngIf="state.type == 'empty'">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triple equals here and everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

[style]="style"
[layout]="layout"

[minZoom]="settings.minZoom"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple ways to do this. Either you can use immtuables for this or override ngDoCheck (https://angular.io/guide/lifecycle-hooks#lifecycle-sequence). It's also ok to leave as-is for now, since this is a bigger change.

boundingBox: undefined,
randomize: true,
componentSpacing: 200,
nodeRepulsion: (node) => 400000,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove params if unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


import {CytoscapeSettings} from './cytoscape-settings.component'

export type GraphViewState = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would not be semantically identical to what is written now. It's ok to keep the discriminator columns and be more precise on types, but in such a scenario, I would split this single type definition into multiple definitions, e.g.

export type Empty = { ... };
export type Loading = {...};
export type GraphViewState = Empty | Loading | ... ;

@@ -0,0 +1,6 @@
<div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I wonder why not make the component element itself the flexbox container?

@franekp franekp force-pushed the graphs-initial branch 3 times, most recently from d39fa22 to 907f279 Compare October 17, 2017 15:33
@franekp franekp force-pushed the graphs-initial branch 4 times, most recently from 4962516 to 1f4c4ef Compare October 17, 2017 16:06
Copy link

@We-St We-St left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks good, but there are some follow-up changes we should consider:

-) Pick a style guide and make the current code conform to it.
-) Replace regular functions with arrow functions wherever sensible.
-) Evaluate if tests can be made non-dependent.
-) Add docs to server-side resources.

Nothing of this is a show-stopper ATM, so approving.

@@ -0,0 +1,91 @@
type DateTime = string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

})

it('should have tests covering all cytoscape options', () => {
expect(Object.keys(remainingOptions).length).toEqual(0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having your tests depend on each other is probably a lot worse than minimal repetition. Especially because you guarantee with your test that the duplicated information stays in sync. (Note that Jasmine is not the problem here. It guarantees execution order, but you cannot be sure that new code will not be added after your test).

If you choose to leave it like this, please add a striking comment to the last test, something like:

// -------------------- THIS TEST NEEDS TO BE LAST --------------------
// This test verifies that all options were properly represented in the
// templates. Due to inter-dependent tests, this NEEDS to go last.

this.zoomChange.emit(this.zoom)
})
for (const [k, v] of Object.entries(this)) {
if (v instanceof EventEmitter && !k.endsWith('Change')) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job, this is quite an improvement.

@franekp franekp force-pushed the graphs-initial branch 2 times, most recently from c30fd49 to a393006 Compare October 17, 2017 16:51
@berggren berggren merged commit 2c949bc into google:master Oct 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants