Browse Source

Better connect dialog and fix for client names.

pull/442/head
Sebastian 6 years ago
parent
commit
ed82a5e61e
  1. 1
      frontend/app/features/settings/declarations.ts
  2. 2
      frontend/app/features/settings/module.ts
  3. 166
      frontend/app/features/settings/pages/clients/client-connect-form.component.html
  4. 64
      frontend/app/features/settings/pages/clients/client-connect-form.component.scss
  5. 105
      frontend/app/features/settings/pages/clients/client-connect-form.component.ts
  6. 72
      frontend/app/features/settings/pages/clients/client.component.html
  7. 10
      frontend/app/features/settings/pages/clients/client.component.scss
  8. 82
      frontend/app/features/settings/pages/clients/client.component.ts
  9. 31
      frontend/app/shared/components/pipes.ts

1
frontend/app/features/settings/declarations.ts

@ -8,6 +8,7 @@
export * from './pages/backups/backup.component';
export * from './pages/backups/backups-page.component';
export * from './pages/clients/client-add-form.component';
export * from './pages/clients/client-connect-form.component';
export * from './pages/clients/client.component';
export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributor-add-form.component';

2
frontend/app/features/settings/module.ts

@ -20,6 +20,7 @@ import {
BackupsPageComponent,
ClientAddFormComponent,
ClientComponent,
ClientConnectFormComponent,
ClientsPageComponent,
ContributorAddFormComponent,
ContributorComponent,
@ -207,6 +208,7 @@ const routes: Routes = [
BackupsPageComponent,
ClientAddFormComponent,
ClientComponent,
ClientConnectFormComponent,
ClientsPageComponent,
ContributorAddFormComponent,
ContributorComponent,

166
frontend/app/features/settings/pages/clients/client-connect-form.component.html

@ -0,0 +1,166 @@
<sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title>
Connect
</ng-container>
<ng-container content>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item breadcrumb-item-done">
<i class="icon-checkmark"></i> Setup client
</li>
<li class="breadcrumb-item breadcrumb-item-active"
[class.breadcrumb-item-active]="isStart"
[class.breadcrumb-item-done]="!isStart">
<i class="icon-checkmark"></i> Choose connection method
</li>
<li class="breadcrumb-item"
[class.breadcrumb-item-active]="!isStart">
<i class="icon-checkmark"></i> Connect
</li>
</ol>
</nav>
<ng-container [ngSwitch]="step">
<ng-container *ngSwitchCase="'Start'">
<h3>Choose a connection method</h3>
<sqx-form-hint>
Start with the Postman tutorial in the <a href="https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman" sqxExternalLink>Documentation</a>
</sqx-form-hint>
<div class="section">
<div class="option" (click)="go('HTTP')">
<h5>Connect manually</h5>
<small class="text-muted">Get instructions how to establish a connection with Postman or curl.</small>
<i class="icon-angle-right"></i>
</div>
<div class="option" (click)="go('CLI')">
<h5>Connect with Squidex CLI</h5>
<small class="text-muted">Download the CLI and connect to this app to start backups, sync schemas or export content.</small>
<i class="icon-angle-right"></i>
</div>
<div class="option" (click)="go('SDK')">
<h5>Connect to your App with SDK</h5>
<small class="text-muted">Download an SDK and establish a connection to this app.</small>
<i class="icon-angle-right"></i>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'HTTP'">
<div class="section">
<h5><span class="badge badge-pill badge-dark">a</span> Get a token using curl</h5>
<p>
<sqx-code>{{connectHttpText}}</sqx-code>
</p>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">b</span> Just use the following token</h5>
<p>
<sqx-code>{{connectToken?.accessToken}}</sqx-code>
</p>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">2</span> Add the token as HTTP header to all requests</h5>
<p>
<sqx-code>Authorization: Bearer [YOUR_TOKEN]</sqx-code>
</p>
</div>
<div class="section">
<sqx-form-hint>
Tokens usally expire after 30days, but you can request multiple tokens.
</sqx-form-hint>
</div>
</ng-container>
<ng-container *ngSwitchCase="'CLI'">
<div class="section">
<h5><span class="badge badge-pill badge-dark">1</span> Get the latest Squidex CLI</h5>
<p>
<a href="https://github.com/Squidex/squidex-samples/releases" sqxExternalLink>Download the CLI from Github</a>
<sqx-form-hint>
The releases contains binaries for all major operation system and a small download if you have .NET Core installed.
</sqx-form-hint>
</p>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">2</span> Add <samp>&lt;your Squidex CLI download directory&gt;</samp> to your <samp>$PATH</samp> variable</h5>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">3</span> Add your app name the CLI config</h5>
<p>
<sqx-code>
sq config add {{appName}} {{appName}}:{{client.id}} ${{client.secret}} -u ${{apiUrl.value}}
</sqx-code>
<sqx-form-hint>
You can manage configuration to multiple apps in the CLI and switch to an app.
</sqx-form-hint>
</p>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">4</span> Switch to your app in the CLI</h5>
<p>
<sqx-code>
sq config use {{appName}}
</sqx-code>
</p>
</div>
</ng-container>
<ng-container *ngSwitchCase="'SDK'">
<div class="section">
<h5><span class="badge badge-pill badge-dark">1</span> Install the .NET SDK</h5>
<p>
The SDK is available on <a href="https://www.nuget.org/packages/Squidex.ClientLibrary/" sqxExternalLink>nuget</a>
</p>
<p>
<sqx-code>dotnet add package Squidex.ClientLibrary</sqx-code>
</p>
</div>
<div class="section">
<h5><span class="badge badge-pill badge-dark">2</span> Create a client manager</h5>
<p>
<sqx-code>{{connectLibraryText}}</sqx-code>
</p>
</div>
<div class="section">
<sqx-form-hint>
Need more SDK? <a href="https://support.squidex.io" sqxExternalLink>Contact us in the Support Forum</a>
</sqx-form-hint>
</div>
</ng-container>
</ng-container>
</ng-container>
<ng-container footer>
<button class="btn btn-secondary" [disabled]="isStart" (click)="go('Start')">
Back
</button>
</ng-container>
</sqx-modal-dialog>

64
frontend/app/features/settings/pages/clients/client-connect-form.component.scss

@ -0,0 +1,64 @@
@import '_vars';
@import '_mixins';
.breadcrumb {
&-item {
i {
display: none;
}
&-done {
color: $color-theme-green;
i {
display: inline-block;
}
}
}
}
.badge {
@include circle(1.2rem);
margin-right: .25rem;
font-size: 80%;
padding-left: 0;
padding-right: 0;
vertical-align: top;
}
.section {
margin-top: 2rem;
p {
padding-left: 1.75rem;
}
}
.option {
@include border-radius;
cursor: pointer;
background: none;
border: 1px solid $color-border;
padding: 1rem;
position: relative;
margin-top: .5rem;
&:hover {
border-color: darken($color-border, 10%);
}
i {
@include absolute(1.6rem, 1rem, auto, auto);
font-size: 1.8rem;
font-weight: 300;
color: $color-border;
}
}
.access-token {
height: 10rem;
font-size: 1rem;
font-weight: normal;
font-family: monospace;
resize: none;
}

105
frontend/app/features/settings/pages/clients/client-connect-form.component.ts

@ -0,0 +1,105 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
AccessTokenDto,
ApiUrlConfig,
AppsState,
ClientDto,
ClientsService,
DialogService,
fadeAnimation,
RoleDto
} from '@app/shared';
@Component({
selector: 'sqx-client-connect-form',
styleUrls: ['./client-connect-form.component.scss'],
templateUrl: './client-connect-form.component.html',
animations: [
fadeAnimation
]
})
export class ClientConnectFormComponent implements OnInit {
@Output()
public complete = new EventEmitter();
@Input()
public client: ClientDto;
@Input()
public clientRoles: ReadonlyArray<RoleDto>;
public appName: string;
public connectToken: AccessTokenDto;
public connectHttpText: string;
public connectLibraryText: string;
public step = 'Start';
public get isStart() {
return this.step === 'Start';
}
constructor(
public readonly appsState: AppsState,
public readonly apiUrl: ApiUrlConfig,
private readonly changeDetector: ChangeDetectorRef,
private readonly clientsService: ClientsService,
private readonly dialogs: DialogService
) {
}
public ngOnInit() {
this.appName = this.appsState.appName;
this.connectHttpText = connectHttpText(this.apiUrl, this.appName, this.client);
this.connectLibraryText = connectLibrary(this.apiUrl, this.appName, this.client);
this.clientsService.createToken(this.appsState.appName, this.client)
.subscribe(dto => {
this.connectToken = dto;
this.changeDetector.detectChanges();
}, error => {
this.dialogs.notifyError(error);
});
}
public go(step: string) {
this.step = step;
}
public emitComplete() {
this.complete.emit();
}
}
function connectHttpText(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) {
const url = apiUrl.buildUrl('identity-server/connect/token');
return `$ curl
-X POST '${url}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=${app}:${client.id}&
client_secret=${client.secret}&
scope=squidex-api`;
}
function connectLibrary(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) {
const url = apiUrl.value;
return `var clientManager = new SquidexClientManager(
"${url}",
"${app}",
"${app}:${client.id}",
"${client.secret}")`;
}

72
frontend/app/features/settings/pages/clients/client.component.html

@ -9,7 +9,7 @@
</sqx-editable-title>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" (click)="connect()">Connect</button>
<button type="button" class="btn btn-primary" (click)="connectDialog.show()">Connect</button>
</div>
<div class="col-auto cell-actions no-padding">
<button type="button" class="btn btn-text-danger"
@ -69,71 +69,7 @@
</div>
<ng-container *sqxModal="connectDialog">
<sqx-modal-dialog (close)="connectDialog.hide()" large="true">
<ng-container title>
Connect
</ng-container>
<ng-container content>
<div class="help">
<h2>How to connect to this client</h2>
<h3>Using HTTP</h3>
<p>
1. Make the following request to get an access token. It will be valid for 30 days.
<sqx-code>{{connectHttpText}}</sqx-code>
</p>
<p>
2. Add the bearer token as authorization header to all requests:
<sqx-code>Authorization: Bearer [YOUR_TOKEN]</sqx-code>
</p>
<p>
Use the following token for testing
<sqx-code>{{connectToken?.accessToken}}</sqx-code>
</p>
<h3>Using the command line interface (CLI)</h3>
<p>
Download the CLI here: <a href="https://github.com/Squidex/squidex-samples/releases" sqxExternalLink>CLI Releases (Linux, OS X, Windows)</a>
</p>
<p>
Connect with .NET SDK:
<sqx-code>{{connectCLINetText}}</sqx-code>
</p>
<p>
Connect with Windows:
<sqx-code>{{connectCLIWinText}}</sqx-code>
</p>
<p>
Connect with Linux / OS X
<sqx-code>{{connectCLINixText}}</sqx-code>
</p>
<h3>Using the C# Client Library</h3>
<p>
Get the nuget library from <a href="https://www.nuget.org/packages/Squidex.ClientLibrary/" sqxExternalLink>nuget.org</a>
</p>
<p>
Create a client manager
<sqx-code>{{connectLibraryText}}</sqx-code>
</p>
</div>
</ng-container>
</sqx-modal-dialog>
<sqx-client-connect-form [client]="client"
(complete)="connectDialog.hide()">
</sqx-client-connect-form>
</ng-container>

10
frontend/app/features/settings/pages/clients/client.component.scss

@ -1,8 +1,6 @@
@import '_vars';
@import '_mixins';
$color-editor: #eceeef;
.card {
& {
@include border-radius(0);
@ -38,11 +36,3 @@ $color-editor: #eceeef;
.cell-input {
padding-right: 0;
}
.access-token {
height: 10rem;
font-size: 1rem;
font-weight: normal;
font-family: monospace;
resize: none;
}

82
frontend/app/features/settings/pages/clients/client.component.ts

@ -5,17 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
AccessTokenDto,
ApiUrlConfig,
AppsState,
ClientDto,
ClientsService,
ClientsState,
DialogModel,
DialogService,
fadeAnimation,
RoleDto
} from '@app/shared';
@ -29,44 +25,21 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ClientComponent implements OnChanges {
export class ClientComponent {
@Input()
public client: ClientDto;
@Input()
public clientRoles: ReadonlyArray<RoleDto>;
public connectToken: AccessTokenDto;
public connectDialog = new DialogModel();
public connectHttpText: string;
public connectCLINixText: string;
public connectCLIWinText: string;
public connectCLINetText: string;
public connectLibraryText: string;
constructor(
public readonly appsState: AppsState,
private readonly apiUrl: ApiUrlConfig,
private readonly changeDetector: ChangeDetectorRef,
private readonly clientsService: ClientsService,
private readonly clientsState: ClientsState,
private readonly dialogs: DialogService
private readonly clientsState: ClientsState
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['client']) {
const app = this.appsState.appName;
this.connectHttpText = connectHttpText(this.apiUrl, app, this.client);
this.connectCLINetText = connectCLINetText(app, this.client, this.apiUrl);
this.connectCLINixText = connectCLINixText(app, this.client, this.apiUrl);
this.connectCLIWinText = connectCLIWinText(app, this.client, this.apiUrl);
this.connectLibraryText = connectLibrary(this.apiUrl, app, this.client);
}
}
public revoke() {
this.clientsState.revoke(this.client);
}
@ -79,54 +52,7 @@ export class ClientComponent implements OnChanges {
this.clientsState.update(this.client, { name });
}
public connect() {
this.connectDialog.show();
this.clientsService.createToken(this.appsState.appName, this.client)
.subscribe(dto => {
this.connectToken = dto;
this.changeDetector.detectChanges();
}, error => {
this.dialogs.notifyError(error);
});
}
public trackByRole(index: number, role: RoleDto) {
public trackByRole(role: RoleDto) {
return role.name;
}
}
function connectHttpText(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) {
const url = apiUrl.buildUrl('identity-server/connect/token');
return `$ curl
-X POST '${url}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=${app}:${client.id}&
client_secret=${client.secret}&
scope=squidex-api`;
}
function connectCLIWinText(app: string, client: { id: string, secret: string }, url: ApiUrlConfig) {
return `.\\sq.exe config add ${app} ${app}:${client.id} ${client.secret} -u ${url.value};.\\sq.exe config use ${app}`;
}
function connectCLINixText(app: string, client: { id: string, secret: string }, url: ApiUrlConfig) {
return `sq config add ${app} ${app}:${client.id} ${client.secret} -u ${url.value} && sq config use ${app}`;
}
function connectCLINetText(app: string, client: { id: string, secret: string }, url: ApiUrlConfig) {
return `dotnet sq.dll config add ${app} ${app}:${client.id} ${client.secret} -u ${url.value}`;
}
function connectLibrary(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) {
const url = apiUrl.value;
return `var clientManager = new SquidexClientManager(
"${url}",
"${app}",
"${app}:${client.id}",
"${client.secret}")`;
}

31
frontend/app/shared/components/pipes.ts

@ -127,15 +127,15 @@ export class UserNameRefPipe extends UserAsyncPipe implements PipeTransform {
public transform(userId: string, placeholder: string | null = 'Me') {
return super.transformInternal(userId, users => {
const parts = userId.split(':');
const { type, id } = split(userId);
if (parts[0] === 'subject') {
return users.getUser(parts[1], placeholder).pipe(map(u => u.displayName));
if (type === 'subject') {
return users.getUser(id, placeholder).pipe(map(u => u.displayName));
} else {
if (parts[1].endsWith('client')) {
return of(parts[1]);
if (id.endsWith('client')) {
return of(id);
} else {
return of(`${parts[1]}-client`);
return of(`${id} client`);
}
}
});
@ -201,10 +201,10 @@ export class UserPictureRefPipe extends UserAsyncPipe implements PipeTransform {
public transform(userId: string) {
return super.transformInternal(userId, users => {
const parts = userId.split(':');
const { type, id } = split(userId);
if (parts[0] === 'subject') {
return users.getUser(parts[1]).pipe(map(u => this.apiUrl.buildUrl(`api/users/${u.id}/picture`)));
if (type === 'subject') {
return users.getUser(id).pipe(map(u => this.apiUrl.buildUrl(`api/users/${u.id}/picture`)));
} else {
return of('./images/client.png');
}
@ -272,3 +272,16 @@ export class FileIconPipe implements PipeTransform {
return `./images/asset_${mimeIcon}.png`;
}
}
function split(token: string) {
const index = token.indexOf(':');
if (index > 0 && index < token.length - 1) {
const type = token.substr(0, index);
const name = token.substr(index + 1);
return { type, id: name };
}
return { type: token, id: token };
}
Loading…
Cancel
Save