Skip to content

Commit

Permalink
login form
Browse files Browse the repository at this point in the history
  • Loading branch information
gaetancollaud committed Feb 8, 2024
1 parent d464ced commit 4656de4
Show file tree
Hide file tree
Showing 25 changed files with 374 additions and 67 deletions.
4 changes: 3 additions & 1 deletion frontend/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ type PricingRuleEntity {
"Query root"
type Query {
contextData: [ContextDataEntity!]!
currentUser: String!
metricNames: [MetricNameEntity!]!
pricingRules: [PricingRuleEntity!]!
}

enum EntityType {
PRINCIPAL
TOPIC
UNKNOWN
}

"Scalar for BigDecimal"
Expand All @@ -72,7 +74,7 @@ input ContextDataDeleteRequestInput {
}

input ContextDataSaveRequestInput {
context: [Entry_String_StringInput]!
context: [Entry_String_StringInput!]!
entityType: EntityType!
id: String
regex: String!
Expand Down
14 changes: 14 additions & 0 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ http{
error_log /var/log/nginx.error.log;
root /usr/share/nginx/html;
charset utf-8;
gzip on;
gzip_types
text/plain
text/css
text/js
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
image/svg+xml/javascript;


add_header X-Frame-Options "DENY";

Expand Down
2 changes: 1 addition & 1 deletion frontend/proxy.conf.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/graphql": {
"target": "http://localhost:8080",
"target": "https://kafka-cost-control-demo.sdm.spoud.io",
"secure": false,
"changeOrigin": true
}
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
<mat-toolbar color="primary">
<span> Kafka cost control </span>

<span class="spacer"></span>
@if (isAuthenticated()) {
<button mat-button class="user-icon" aria-label="Logout" matTooltip="Sign out" (click)="signOut()">
Sign out
<mat-icon>logout</mat-icon>
</button>
} @else {
<button mat-button class="user-icon" aria-label="Login" matTooltip="Sign in" (click)="signIn()">
Sign in
<mat-icon>login</mat-icon>
</button>
}
</mat-toolbar>

<nav mat-tab-nav-bar [tabPanel]="tabPanel">
@for (link of navLinks; track link) {
@for (link of navLinksSignal(); track link) {
<a mat-tab-link
[routerLink]="link.path"
routerLinkActive #rla="routerLinkActive"
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/app.component.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
nav{
background: white;
}
mat-tab-nav-panel{
padding: 32px 64px;
display: block;
}
.spacer {
flex: 1 1 auto;
}
49 changes: 35 additions & 14 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import {Component} from '@angular/core';
import {Component, computed, Signal} from '@angular/core';
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {CommonModule} from '@angular/common';
import {MatTabNav, MatTabNavPanel, MatTabsModule} from '@angular/material/tabs';
import {MatToolbar, MatToolbarModule} from '@angular/material/toolbar';
import {MatTabsModule} from '@angular/material/tabs';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {BasicAuthServiceService} from './services/basic-auth-service.service';
import {MatTooltip} from '@angular/material/tooltip';
import {MatDialog} from '@angular/material/dialog';
import {SignInDialogComponent} from './common/sign-in-dialog/sign-in-dialog.component';

interface Link {
path: string;
Expand All @@ -23,24 +29,39 @@ interface Link {
RouterOutlet,

MatTabsModule,
MatToolbar
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatTooltip
],
})
export class AppComponent {
navLinks: Link[] = [
{path: '/context-data', label: 'Context Data'},
{path: '/pricing-rules', label: 'Pricing Rules'},
{path: '/others', label: 'Others'},
]
activeLink: Link = this.navLinks[0];
isAuthenticated: Signal<boolean>;
navLinksSignal = computed(() => {
const list = [
{path: '/context-data', label: 'Context Data'},
{path: '/pricing-rules', label: 'Pricing Rules'},
];
if (this.isAuthenticated()) {
list.push({path: '/others', label: 'Others'});

constructor(private router: Router) {
}
return list;
})

constructor(private _router: Router, private _dialog: MatDialog, private _authService: BasicAuthServiceService) {
this.isAuthenticated = _authService.authenticated();
}

ngOnInit(): void {
this.router.events.subscribe((res) => {
this.activeLink = this.navLinks.find(tab => tab.path === '.' + this.router.url) || this.activeLink;
signOut(): void {
this._authService.signOut();
}

signIn(): void {
const dialogRef = this._dialog.open(SignInDialogComponent);

dialogRef.afterClosed().subscribe({
next: (result) => console.log('Sign in dialog closed', result)
});
}
}
15 changes: 15 additions & 0 deletions frontend/src/app/common/key-value-list/key-value-list.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Component, input} from '@angular/core';
import {Entry_String_String} from '../../../generated/graphql/types';

@Component({
selector: 'app-key-value-list',
standalone: true,
imports: [],
templateUrl: './key-value-list.component.html',
styleUrl: './key-value-list.component.scss'
})
export class KeyValueListComponent {

entries = input.required<Entry_String_String[]>();

}
15 changes: 0 additions & 15 deletions frontend/src/app/common/map-display/map-display.component.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<h1 mat-dialog-title>Login</h1>
<div mat-dialog-content>
<form (ngSubmit)="signIn()">
<mat-form-field>
<mat-label>Username</mat-label>
<input autocomplete="username" matInput [formControl]="username" cdkFocusInitial>
</mat-form-field>
<mat-form-field>
<mat-label>Password</mat-label>
<input autocomplete="password" type="password" matInput [formControl]="password">
</mat-form-field>
<button type="summit" mat-raised-button color="primary" [disabled]="username.invalid || password.invalid" (click)="signIn()" >Sign in</button>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
form {
display: flex;
flex-direction: column;
align-items: center;
}

62 changes: 62 additions & 0 deletions frontend/src/app/common/sign-in-dialog/sign-in-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {Component} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {MatDialogActions, MatDialogClose, MatDialogContent, MatDialogTitle} from "@angular/material/dialog";
import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/material/datepicker';
import {MatFormField, MatHint, MatLabel, MatSuffix} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {BasicAuthServiceService} from '../../services/basic-auth-service.service';
import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';
import {DialogRef} from '@angular/cdk/dialog';

@Component({
selector: 'app-sign-in-dialog',
standalone: true,
imports: [
ReactiveFormsModule,
MatSnackBarModule,

MatButton,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose,
FormsModule,
MatDatepicker,
MatDatepickerInput,
MatDatepickerToggle,
MatFormField,
MatHint,
MatInput,
MatLabel,
MatSuffix,
],
templateUrl: './sign-in-dialog.component.html',
styleUrl: './sign-in-dialog.component.scss'
})
export class SignInDialogComponent {

constructor(private _dialogRef: DialogRef<SignInDialogComponent>, private _authService: BasicAuthServiceService, private _snakbar: MatSnackBar) {
}

username = new FormControl('', [Validators.required]);
password = new FormControl('', [Validators.required]);

signIn() {
this._authService.signIn(this.username.value || '', this.password.value || '').subscribe({
next: (result) => {
this._snakbar.open('Sign in success', 'close', {
politeness: 'polite',
duration: 2000,
});
this._dialogRef.close();
},
error: (err) => {
this._snakbar.open('Sign in failed: ' + err.message, 'close', {
politeness: 'assertive',
duration: 5000,
});
}
});
}
}
30 changes: 16 additions & 14 deletions frontend/src/app/graphql-provider.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import {Provider} from '@angular/core';
import {Apollo, APOLLO_OPTIONS} from 'apollo-angular';
import {ApolloClientOptions, createHttpLink, InMemoryCache} from '@apollo/client/core';
import {ApolloClientOptions, ApolloLink, createHttpLink, InMemoryCache} from '@apollo/client/core';
import {setContext} from '@apollo/client/link/context';
import generatedFragments from '../generated/graphql/fragments';
import {AdditionalHeadersService} from './services/additional-headers.service';


const httpLink = createHttpLink({
uri: '/graphql',
});

const authLink = setContext((_, {headers}) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Basic ${token}` : '',
function authLink(additionalHeadersService: AdditionalHeadersService): ApolloLink {
return setContext((_, {headers}) => {
return {
headers: {
...headers,
...additionalHeadersService.getHeaders(),
}
}
}
});
});
}

export function createApollo(): ApolloClientOptions<any> {
export function createApollo(additionalHeadersService: AdditionalHeadersService): ApolloClientOptions<any> {
return {
link: authLink.concat(httpLink),
link: authLink(additionalHeadersService).concat(httpLink),
cache: new InMemoryCache({
possibleTypes: generatedFragments.possibleTypes,
}),
Expand All @@ -42,7 +42,9 @@ export function provideGraphql(): Provider[] {
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [],
deps: [
AdditionalHeadersService
],
},
{
provide: Apollo,
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/app/services/additional-header.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AdditionalHeadersService } from './additional-headers.service';

describe('AdditionalHeaderService', () => {
let service: AdditionalHeadersService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AdditionalHeadersService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
21 changes: 21 additions & 0 deletions frontend/src/app/services/additional-headers.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Injectable} from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class AdditionalHeadersService {

private _headers: { [key: string]: string } = {};

public setHeader(key: string, value: string): void {
this._headers[key] = value;
}

public getHeaders(): { [key: string]: string } {
return this._headers;
}

public removeHeader(key: string) {
delete this._headers[key];
}
}
16 changes: 16 additions & 0 deletions frontend/src/app/services/basic-auth-service.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { BasicAuthServiceService } from './basic-auth-service.service';

describe('BasicAuthServiceService', () => {
let service: BasicAuthServiceService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(BasicAuthServiceService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Loading

0 comments on commit 4656de4

Please sign in to comment.