-
Notifications
You must be signed in to change notification settings - Fork 593
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
Conversation
4c4fead
to
1254502
Compare
@@ -0,0 +1,42 @@ | |||
/* | |||
Copyright 2015 Google Inc. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/2015/2017
1254502
to
6b321b5
Compare
There was a problem hiding this 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>> { |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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'] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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.
timesketch/ui/api/graph.service.ts
Outdated
|
||
@Injectable() | ||
export class GraphService { | ||
constructor(private sketchService: SketchService, private http: HttpClient) {} |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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')) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
?
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting.
There was a problem hiding this comment.
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 /> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Close tag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
87b40d4
to
7d777ea
Compare
@berggren Ready to merge. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
timesketch/ui/css/ts.css
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. But later.
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = [ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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?: ...
}
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 | ... ;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
586e1da
to
905084a
Compare
@@ -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 () { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
timesketch/ui/css/ts.scss
Outdated
div { | ||
@include ts-graphs-fill-space | ||
} | ||
@include ts-graphs-fill-space |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolons here.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The event is unused.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove params if unused.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
d39fa22
to
907f279
Compare
4962516
to
1f4c4ef
Compare
1f4c4ef
to
1e32eb1
Compare
There was a problem hiding this 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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')) { |
There was a problem hiding this comment.
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.
c30fd49
to
a393006
Compare
a393006
to
c9f39a7
Compare
No description provided.