diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 147d0e57c5..25afa050f6 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -3,7 +3,10 @@ "api.generalApi": "General API", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "All Apps", "apps.allTeams": "All Teams", @@ -245,11 +248,13 @@ "common.cancelAll": "Cancel All", "common.category": "Category", "common.clear": "Clear", + "common.client": "Client", "common.clientId": "Client Id", "common.clients": "Clients", "common.clientSecret": "Client Secret", "common.clipboardAdded": "Value has been added to your clipboard.", "common.clone": "Clone", + "common.close": "Close", "common.cluster": "Cluster", "common.clusterPageTitle": "Cluster", "common.comments": "Comments", diff --git a/backend/i18n/frontend_fr.json b/backend/i18n/frontend_fr.json index e332642442..0cabaa9724 100644 --- a/backend/i18n/frontend_fr.json +++ b/backend/i18n/frontend_fr.json @@ -3,7 +3,10 @@ "api.generalApi": "API générale", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "Toutes les applications", "apps.allTeams": "Toutes les équipes", @@ -245,11 +248,13 @@ "common.cancelAll": "Annuler tout", "common.category": "Catégorie", "common.clear": "Clair", + "common.client": "Client", "common.clientId": "Identité du client", "common.clients": "Clients", "common.clientSecret": "Clé secrète du client", "common.clipboardAdded": "La valeur a été ajoutée à votre presse-papiers.", "common.clone": "Cloner", + "common.close": "Close", "common.cluster": "Grappe", "common.clusterPageTitle": "Grappe", "common.comments": "commentaires", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 4d6477ef53..726549dc2e 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -3,7 +3,10 @@ "api.generalApi": "API generali", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "Tutte le Apps", "apps.allTeams": "All Teams", @@ -245,11 +248,13 @@ "common.cancelAll": "Cancel All", "common.category": "Categoria", "common.clear": "Pulisci", + "common.client": "Client", "common.clientId": "Client Id", "common.clients": "Client", "common.clientSecret": "Client Secret", "common.clipboardAdded": "Il valore è stato aggiunto nei tuoi appunti.", "common.clone": "Clona", + "common.close": "Close", "common.cluster": "Cluster", "common.clusterPageTitle": "Cluster", "common.comments": "Commenti", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index da3b8313e1..2ac3826215 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -3,7 +3,10 @@ "api.generalApi": "Algemene API", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "Alle apps", "apps.allTeams": "All Teams", @@ -245,11 +248,13 @@ "common.cancelAll": "Annulleer Alles", "common.category": "Categorie", "common.clear": "Wissen", + "common.client": "Client", "common.clientId": "Client-ID", "common.clients": "Clients", "common.clientSecret": "Clientgeheim", "common.clipboardAdded": "Waarde is toegevoegd aan uw klembord.", "common.clone": "Kloon", + "common.close": "Close", "common.cluster": "Cluster", "common.clusterPageTitle": "Cluster", "common.comments": "Reacties", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index 2d385470f0..0f3f7a5a7d 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -3,7 +3,10 @@ "api.generalApi": "API Geral", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "Todas as aplicações", "apps.allTeams": "Todas as Equipas", @@ -245,11 +248,13 @@ "common.cancelAll": "cancelar todos", "common.category": "Categorias", "common.clear": "Claro", + "common.client": "Client", "common.clientId": "ID do cliente", "common.clients": "Clientes", "common.clientSecret": "Segredo do Cliente", "common.clipboardAdded": "O valor foi adicionado à sua área de transferência.", "common.clone": "Clone", + "common.close": "Close", "common.cluster": "Cluster", "common.clusterPageTitle": "Cluster", "common.comments": "Comentários", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index df48bf1058..e704c15483 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -3,7 +3,10 @@ "api.generalApi": "通用 API", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "所有应用程序", "apps.allTeams": "All Teams", @@ -245,11 +248,13 @@ "common.cancelAll": "Cancel All", "common.category": "类别", "common.clear": "清除", + "common.client": "Client", "common.clientId": "客户端 ID", "common.clients": "客户端", "common.clientSecret": "客户端密码", "common.clipboardAdded": "值已添加到您的剪贴板。", "common.clone": "克隆", + "common.close": "Close", "common.cluster": "集群", "common.clusterPageTitle": "集群", "common.comments": "评论", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 147d0e57c5..25afa050f6 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -3,7 +3,10 @@ "api.generalApi": "General API", "api.graphql": "GraphQL", "api.graphqlPageTitle": "GraphQL", + "api.noClient": "Token of current user", "api.pageTitle": "API", + "api.selectClient": "Select Client", + "api.selectClientDescription": "Select a client that should be used for authentication. Can be used to simulate permissions and headers.", "api.title": "API", "apps.allApps": "All Apps", "apps.allTeams": "All Teams", @@ -245,11 +248,13 @@ "common.cancelAll": "Cancel All", "common.category": "Category", "common.clear": "Clear", + "common.client": "Client", "common.clientId": "Client Id", "common.clients": "Clients", "common.clientSecret": "Client Secret", "common.clipboardAdded": "Value has been added to your clipboard.", "common.clone": "Clone", + "common.close": "Close", "common.cluster": "Cluster", "common.clusterPageTitle": "Cluster", "common.comments": "Comments", diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs index 3694b02570..315bbea833 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs @@ -59,8 +59,8 @@ public AcceptHeader_NoSlowTotal() public class AcceptHeaderAttribute : OpenApiOperationProcessorAttribute { - public AcceptHeaderAttribute(string name, string description, JsonObjectType type = JsonObjectType.String) - : base(typeof(Processor), name, description, type) + public AcceptHeaderAttribute(string name, string description, JsonObjectType schemaType = JsonObjectType.String) + : base(typeof(Processor), name, description, schemaType) { } @@ -68,27 +68,30 @@ public sealed class Processor : IOperationProcessor { private readonly string name; private readonly string description; - private readonly JsonObjectType type; + private readonly JsonObjectType schemaType; - public Processor(string name, string description, JsonObjectType type) + public Processor(string name, string description, JsonObjectType schemaType) { this.name = name; this.description = description; - this.type = type; + this.schemaType = schemaType; } public bool Process(OperationProcessorContext context) { - context.OperationDescription.Operation.Parameters.Add(new OpenApiParameter + var parameter = new OpenApiParameter { Name = name, Kind = OpenApiParameterKind.Header, Schema = new JsonSchema { - Type = type + Type = schemaType }, - Description = description, - }); + Description = description + }; + + context.OperationDescription.Operation.Parameters.Add(parameter); + context.OperationDescription.Operation.SetPositions(); return true; } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs index d157a180e8..54d34cb1ae 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs @@ -88,5 +88,17 @@ void AddParameterQuery(OpenApiParameter parameter) Name = "ids", Description = FieldDescriptions.QueryIds }); + + operation.SetPositions(); + } + + public static void SetPositions(this OpenApiOperation operation) + { + var position = 0; + + foreach (var parameter in operation.Parameters) + { + parameter.Position = position++; + } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs index 258bfc6fb1..8ac77c5485 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs @@ -84,6 +84,7 @@ private OperationBuilder AddParameter(string name, JsonSchema schema, OpenApiPar } operation.Parameters.Add(parameter); + operation.SetPositions(); return this; } diff --git a/frontend/src/app/features/api/pages/graphql/graphql-page.component.html b/frontend/src/app/features/api/pages/graphql/graphql-page.component.html index 43cd5939be..fb39b977f4 100644 --- a/frontend/src/app/features/api/pages/graphql/graphql-page.component.html +++ b/frontend/src/app/features/api/pages/graphql/graphql-page.component.html @@ -2,4 +2,39 @@
-
\ No newline at end of file + + + + + + + + {{ 'api.selectClient' | sqxTranslate }} + + + + + {{ 'api.selectClientDescription' | sqxTranslate }} + + +
+ + + +
+
+ + + + +
+
\ No newline at end of file diff --git a/frontend/src/app/features/api/pages/graphql/graphql-page.component.scss b/frontend/src/app/features/api/pages/graphql/graphql-page.component.scss index db605a53bc..51782baacf 100644 --- a/frontend/src/app/features/api/pages/graphql/graphql-page.component.scss +++ b/frontend/src/app/features/api/pages/graphql/graphql-page.component.scss @@ -12,6 +12,7 @@ --color-primary: #{hsl-str($color-theme-brand)}; --color-success: #{hsl-str($color-theme-success)}; --color-error: #{hsl-str($color-theme-error)}; + --color-neutral: #{hsl-str($color-text)}; --border-radius-4: #{$border-radius}; --border-radius-8: #{$border-radius}; --border-radius-12: #{$border-radius}; @@ -19,9 +20,24 @@ .graphiql-sessions { border-radius: $border-radius; } + + * { + box-sizing: content-box; + } } .graphiql-logo { display: none; } +} + +.btn-options { + @include absolute(null, null, 175px, 8px); + font-size: 1.2rem; + font-weight: normal; + width: 44px; + + &:hover { + background: rgba(59, 75, 104, 7%); + } } \ No newline at end of file diff --git a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts b/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts index 0c1272f891..7a41742b42 100644 --- a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts +++ b/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts @@ -5,44 +5,77 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { createGraphiQLFetcher } from '@graphiql/toolkit'; import GraphiQL from 'graphiql'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { ApiUrlConfig, AppsState, AuthService } from '@app/shared'; +import { ApiUrlConfig, AppsState, AuthService, ClientDto, ClientsService, ClientsState, DialogModel } from '@app/shared'; @Component({ selector: 'sqx-graphql-page', styleUrls: ['./graphql-page.component.scss'], templateUrl: './graphql-page.component.html', }) -export class GraphQLPageComponent implements AfterViewInit { +export class GraphQLPageComponent implements AfterViewInit, OnInit { @ViewChild('graphiQLContainer', { static: false }) public graphiQLContainer!: ElementRef; + public clientsReadable = false; + public clientsDialog = new DialogModel(); + public clientSelected: ClientDto | null = null; + constructor( private readonly appsState: AppsState, private readonly apiUrl: ApiUrlConfig, private readonly authService: AuthService, + private readonly clientsService: ClientsService, + public readonly clientsState: ClientsState, ) { } + public ngOnInit() { + this.clientsReadable = this.appsState.snapshot.selectedApp!.canReadClients; + + if (this.clientsReadable) { + this.clientsState.load(); + } + } + public ngAfterViewInit() { - const url = this.apiUrl.buildUrl(`api/content/${this.appsState.appName}/graphql`); + this.selectClient(null); + } + + public selectClient(client: ClientDto | null) { + this.clientSelected = client; + + if (!client) { + this.initOrUpdateGraphQL(this.authService.user?.accessToken!); + } else { + this.clientsService.createToken(this.appsState.appName, client) + .subscribe(token => { + if (this.clientSelected === client) { + this.initOrUpdateGraphQL(token.accessToken); + } + }); + } + } + + private initOrUpdateGraphQL(accessToken: string) { + const graphQLEndpoint = this.apiUrl.buildUrl(`api/content/${this.appsState.appName}/graphql`); const subscriptionUrl = - url + graphQLEndpoint .replace('http://', 'ws://') .replace('https://', 'wss://') + - `?access_token=${this.authService.user?.accessToken}`; + `?access_token=${accessToken}`; const fetcher = createGraphiQLFetcher({ - url, - subscriptionUrl, + url: graphQLEndpoint, headers: { - Authorization: `Bearer ${this.authService.user?.accessToken}`, + Authorization: `Bearer ${accessToken}`, }, + subscriptionUrl, }); ReactDOM.render(