diff --git a/src/apim.runtime.module.ts b/src/apim.runtime.module.ts
index c4d9b5e57..679a73240 100644
--- a/src/apim.runtime.module.ts
+++ b/src/apim.runtime.module.ts
@@ -1,9 +1,9 @@
+import "./polyfills";
import "@paperbits/core/ko/bindingHandlers/bindingHandlers.activate";
import "@paperbits/core/ko/bindingHandlers/bindingHandlers.component";
import "@paperbits/core/ko/bindingHandlers/bindingHandlers.dialog";
import "@paperbits/core/ko/bindingHandlers/bindingHandlers.focus";
import "@paperbits/core/ko/bindingHandlers/bindingHandlers.scrollable";
-import { DefaultSettingsProvider } from "@paperbits/common/configuration";
import { IInjector, IInjectorModule } from "@paperbits/common/injection";
import { ConsoleLogger } from "@paperbits/common/logging";
import { DefaultSessionManager } from "@paperbits/common/persistence/defaultSessionManager";
@@ -53,7 +53,6 @@ import { Signup } from "./components/users/signup/ko/runtime/signup";
import { Subscriptions } from "./components/users/subscriptions/ko/runtime/subscriptions";
import { ValidationSummary } from "./components/users/validation-summary/ko/runtime/validation-summary";
import { UnhandledErrorHandler } from "./errors/unhandledErrorHandler";
-import "./polyfills";
import { AadSignOutRouteGuard } from "./routing/aadSignoutRouteGuard";
import { RouteHelper } from "./routing/routeHelper";
import { SignOutRouteGuard } from "./routing/signOutRouteGuard";
@@ -69,6 +68,7 @@ import { ProvisionService } from "./services/provisioningService";
import { TagService } from "./services/tagService";
import { TenantService } from "./services/tenantService";
import { UsersService } from "./services/usersService";
+import { ApimSettingsProvider } from "./configuration/apimSettingsProvider";
export class ApimRuntimeModule implements IInjectorModule {
public register(injector: IInjector): void {
@@ -122,7 +122,7 @@ export class ApimRuntimeModule implements IInjectorModule {
injector.bindSingleton("backendService", BackendService);
injector.bindSingleton("aadService", AadService);
injector.bindSingleton("mapiClient", MapiClient);
- injector.bindSingleton("settingsProvider", DefaultSettingsProvider);
+ injector.bindSingleton("settingsProvider", ApimSettingsProvider);
injector.bindSingleton("authenticator", DefaultAuthenticator);
injector.bindSingleton("routeHelper", RouteHelper);
injector.bindSingleton("userService", StaticUserService);
diff --git a/src/components/app/app.ts b/src/components/app/app.ts
index 9304d59f8..98a34ca09 100644
--- a/src/components/app/app.ts
+++ b/src/components/app/app.ts
@@ -1,3 +1,4 @@
+import { EventManager } from "@paperbits/common/events";
import { AccessToken } from "./../../authentication/accessToken";
import template from "./app.html";
import { ViewManager } from "@paperbits/common/ui";
@@ -5,7 +6,6 @@ import { Component, OnMounted } from "@paperbits/common/ko/decorators";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { ISiteService } from "@paperbits/common/sites";
import { IAuthenticator } from "../../authentication";
-import { Utils } from "../../utils";
const startupError = `Unable to start the portal`;
@@ -18,7 +18,8 @@ export class App {
private readonly settingsProvider: ISettingsProvider,
private readonly authenticator: IAuthenticator,
private readonly viewManager: ViewManager,
- private readonly siteService: ISiteService
+ private readonly siteService: ISiteService,
+ private readonly eventManager: EventManager
) { }
@OnMounted()
@@ -70,6 +71,11 @@ export class App {
this.viewManager.setHost({ name: "page-host" });
this.viewManager.showToolboxes();
+
+ setTimeout(() => this.eventManager.dispatchEvent("displayHint", {
+ key: "a69b",
+ content: `When you're in the administrative view, you still can navigate any website hyperlink by clicking on it holding Ctrl (Windows) or ⌘ (Mac) key.`
+ }), 5000);
}
catch (error) {
this.viewManager.addToast(startupError, `Check if the settings specified in the configuration file config.design.json are correct or refer to the frequently asked questions.`);
diff --git a/src/components/content/content.ts b/src/components/content/content.ts
index fb5d9303f..a54898cb8 100644
--- a/src/components/content/content.ts
+++ b/src/components/content/content.ts
@@ -1,12 +1,13 @@
import template from "./content.html";
+import * as moment from "moment";
+import * as Constants from "../../constants";
import { ViewManager, View } from "@paperbits/common/ui";
import { Component } from "@paperbits/common/ko/decorators";
-import { HttpClient } from "@paperbits/common/http";
-import { ISettingsProvider } from "@paperbits/common/configuration";
import { Logger } from "@paperbits/common/logging";
import { IAuthenticator } from "../../authentication/IAuthenticator";
import { AppError } from "./../../errors/appError";
import { MapiError } from "../../errors/mapiError";
+import { MapiClient } from "../../services";
@Component({
@@ -16,9 +17,8 @@ import { MapiError } from "../../errors/mapiError";
export class ContentWorkshop {
constructor(
private readonly viewManager: ViewManager,
- private readonly httpClient: HttpClient,
+ private readonly mapiClient: MapiClient,
private readonly authenticator: IAuthenticator,
- private readonly settingsProvider: ISettingsProvider,
private readonly logger: Logger
) { }
@@ -30,20 +30,12 @@ export class ContentWorkshop {
}
try {
- const accessToken = await this.authenticator.getAccessTokenAsString();
+ const revisionName = moment.utc().format(Constants.releaseNameFormat);
- const publishRootUrl = await this.settingsProvider.getSetting("backendUrl") || "";
-
- const response = await this.httpClient.send({
- url: publishRootUrl + "/publish",
- method: "POST",
- headers: [{ name: "Authorization", value: accessToken }]
+ await this.mapiClient.put(`/portalRevisions/${revisionName}`, null, {
+ properties: { description: "" }
});
- if (response.statusCode !== 200) {
- throw MapiError.fromResponse(response);
- }
-
this.viewManager.notifySuccess("Operations", `The website is being published...`);
this.viewManager.closeWorkshop("content-workshop");
}
diff --git a/src/configuration/apimSettingsProvider.ts b/src/configuration/apimSettingsProvider.ts
new file mode 100644
index 000000000..074c400f5
--- /dev/null
+++ b/src/configuration/apimSettingsProvider.ts
@@ -0,0 +1,72 @@
+import * as Objects from "@paperbits/common/objects";
+import { EventManager } from "@paperbits/common/events";
+import { HttpClient } from "@paperbits/common/http";
+import { ISettingsProvider } from "@paperbits/common/configuration";
+import { SessionManager } from "@paperbits/common/persistence/sessionManager";
+
+
+export class ApimSettingsProvider implements ISettingsProvider {
+ private configuration: Object;
+ private initializePromise: Promise;
+
+ constructor(
+ private readonly httpClient: HttpClient,
+ private readonly eventManager: EventManager,
+ private readonly sessionManager: SessionManager
+ ) { }
+
+ private async ensureInitialized(): Promise {
+ if (!this.initializePromise) {
+ this.initializePromise = this.loadSettings();
+ }
+ return this.initializePromise;
+ }
+
+ private async loadSettings(): Promise {
+ const commonConfigurationResponse = await this.httpClient.send({ url: "/config.json" });
+ const commonConfiguration = commonConfigurationResponse.toObject();
+
+ const searializedDesignTimeSettings = await this.sessionManager?.getItem("designTimeSettings");
+
+ if (searializedDesignTimeSettings) {
+ const designTimeSettings = searializedDesignTimeSettings;
+ Object.assign(commonConfiguration, designTimeSettings);
+ }
+ else {
+ const apimsConfigurationResponse = await this.httpClient.send({ url: "/config-apim.json" });
+
+ if (apimsConfigurationResponse.statusCode === 200) {
+ const apimConfiguration = apimsConfigurationResponse.toObject();
+ Object.assign(commonConfiguration, apimConfiguration);
+ }
+ }
+
+ this.configuration = commonConfiguration;
+ }
+
+ public async getSetting(name: string): Promise {
+ await this.ensureInitialized();
+ return Objects.getObjectAt(name, this.configuration);
+ }
+
+ public onSettingChange(name: string, eventHandler: (value: T) => void): void {
+ this.eventManager.addEventListener("onSettingChange", (setting) => {
+ if (setting.name === name) {
+ eventHandler(setting.value);
+ }
+ });
+ }
+
+ public async setSetting(name: string, value: T): Promise {
+ await this.ensureInitialized();
+
+ Objects.setValue(name, this.configuration, value);
+ this.eventManager.dispatchEvent("onSettingChange", { name: name, value: value });
+ }
+
+ public async getSettings(): Promise {
+ await this.ensureInitialized();
+
+ return this.configuration;
+ }
+}
\ No newline at end of file
diff --git a/src/constants.ts b/src/constants.ts
index 0ec9f0d36..84ebdc16f 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -134,4 +134,6 @@ export const developerPortalType = "self-hosted-portal";
/**
* Header name to track developer portal type.
*/
-export const portalHeaderName = "x-ms-apim-client";
\ No newline at end of file
+export const portalHeaderName = "x-ms-apim-client";
+
+export const releaseNameFormat = "YYYYMMDDHHmm";
\ No newline at end of file
diff --git a/src/contracts/oauthSession.ts b/src/contracts/oauthSession.ts
new file mode 100644
index 000000000..c48338005
--- /dev/null
+++ b/src/contracts/oauthSession.ts
@@ -0,0 +1,11 @@
+export interface OAuthSession {
+ authenticationFlow: string;
+ authenticationCallback: (accessToken: string) => void;
+ authenticationErrorCallback: (error: Error) => void;
+ loginUrl: string;
+ redirectUri: string;
+ clientId: string;
+ issuer: string;
+ tokenEndpoint: string;
+ scope: string;
+}
\ No newline at end of file
diff --git a/src/models/service.ts b/src/models/service.ts
new file mode 100644
index 000000000..656cbf01a
--- /dev/null
+++ b/src/models/service.ts
@@ -0,0 +1,15 @@
+import { ServiceDescriptionContract, HostnameConfiguration, ServiceSku } from "./../contracts/service";
+
+export class ServiceDescription {
+ public name: string;
+ public hostnameConfigurations: HostnameConfiguration[];
+ public sku: ServiceSku;
+ public gatewayUrl: string;
+
+ constructor(contract: ServiceDescriptionContract) {
+ this.name = contract.name;
+ this.sku = contract.sku;
+ this.gatewayUrl = contract.properties.gatewayUrl;
+ this.hostnameConfigurations = contract.properties.hostnameConfigurations;
+ }
+}
\ No newline at end of file
diff --git a/src/modules.d.ts b/src/modules.d.ts
index d4dabb326..6e4c92570 100644
--- a/src/modules.d.ts
+++ b/src/modules.d.ts
@@ -11,4 +11,9 @@ declare module "*.liquid" {
declare module "*.txt" {
const content: string;
export default content;
+}
+
+declare module "*.raw" {
+ const content: string;
+ export default content;
}
\ No newline at end of file
diff --git a/src/persistence/cachedObjectStorage.ts b/src/persistence/cachedObjectStorage.ts
new file mode 100644
index 000000000..7b4413fce
--- /dev/null
+++ b/src/persistence/cachedObjectStorage.ts
@@ -0,0 +1,42 @@
+import { LruCache } from "@paperbits/common/caching";
+import { IObjectStorage, Page, Query } from "@paperbits/common/persistence";
+
+export class CachedObjectStorage {
+ private readonly cache: LruCache>;
+
+ constructor(
+ private readonly underlyingStorage: IObjectStorage
+ ) {
+ this.cache = new LruCache(100, () => { return; });
+ }
+
+ public addObject(key: string, dataObject: T): Promise {
+ throw new Error("Not supported.");
+ }
+
+ public getObject(key: string): Promise {
+ const cachedItemPromise = this.cache.getItem(key);
+
+ if (cachedItemPromise) {
+ return cachedItemPromise;
+ }
+
+ const fetchPromise = this.underlyingStorage.getObject(key);
+
+ this.cache.setItem(key, fetchPromise);
+
+ return fetchPromise;
+ }
+
+ public deleteObject(key: string): Promise {
+ throw new Error("Not supported.");
+ }
+
+ public updateObject(key: string, dataObject: T): Promise {
+ throw new Error("Not supported.");
+ }
+
+ public async searchObjects(key: string, query?: Query): Promise> {
+ return await this.underlyingStorage.searchObjects(key, query);
+ }
+}
\ No newline at end of file
diff --git a/src/persistence/publishingCacheModule.ts b/src/persistence/publishingCacheModule.ts
new file mode 100644
index 000000000..da9bcb675
--- /dev/null
+++ b/src/persistence/publishingCacheModule.ts
@@ -0,0 +1,14 @@
+import { IObjectStorage } from "@paperbits/common/persistence";
+import { IInjector, IInjectorModule } from "@paperbits/common/injection";
+import { CachedObjectStorage } from "./cachedObjectStorage";
+
+
+export class PublishingCacheModule implements IInjectorModule {
+ public register(injector: IInjector): void {
+ const underlyingObjectStorage = injector.resolve("objectStorage");
+
+ injector.bindSingletonFactory("objectStorage", (ctx: IInjector) => {
+ return new CachedObjectStorage(underlyingObjectStorage);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/publishing/aadConfigPublisher.ts b/src/publishing/aadConfigPublisher.ts
index b3b92a613..e2b0762f0 100644
--- a/src/publishing/aadConfigPublisher.ts
+++ b/src/publishing/aadConfigPublisher.ts
@@ -13,14 +13,10 @@ import { RuntimeConfigBuilder } from "./runtimeConfigBuilder";
export class AadConfigPublisher implements IPublisher {
constructor(
private readonly runtimeConfigBuilder: RuntimeConfigBuilder,
- private readonly identityService: IdentityService,
- private readonly settingsProvider: ISettingsProvider
+ private readonly identityService: IdentityService
) { }
public async publish(): Promise {
- const managementApiUrl = await this.settingsProvider.getSetting(SettingNames.managementApiUrl);
- this.runtimeConfigBuilder.addSetting(SettingNames.managementApiUrl, managementApiUrl);
-
const identityProviders = await this.identityService.getIdentityProviders();
const aadIdentityProvider = identityProviders.find(x => x.type === SettingNames.aadClientConfig);
diff --git a/src/publishing/runtimeConfigPublisher.ts b/src/publishing/runtimeConfigPublisher.ts
index 53da084f6..fafe721d5 100644
--- a/src/publishing/runtimeConfigPublisher.ts
+++ b/src/publishing/runtimeConfigPublisher.ts
@@ -16,6 +16,6 @@ export class RuntimeConfigPublisher implements IPublisher {
const configuration = this.runtimeConfigBuilder.build();
const content = Utils.stringToUnit8Array(JSON.stringify(configuration));
- await this.outputBlobStorage.uploadBlob("/config.json", content);
+ await this.outputBlobStorage.uploadBlob("/config-apim.json", content);
}
}
\ No newline at end of file
diff --git a/src/services/runtimeConfigurator.ts b/src/services/runtimeConfigurator.ts
index 09bf36873..a84633a72 100644
--- a/src/services/runtimeConfigurator.ts
+++ b/src/services/runtimeConfigurator.ts
@@ -63,6 +63,6 @@ export class RuntimeConfigurator {
designTimeSettings[SettingNames.aadB2CClientConfig] = aadB2CConfig;
}
- this.sessionManager.setItem("designTimeSettings", JSON.stringify(designTimeSettings));
+ this.sessionManager.setItem("designTimeSettings", designTimeSettings);
}
}
\ No newline at end of file
diff --git a/src/startup.publish.ts b/src/startup.publish.ts
index 13d3c12e0..afbb17715 100644
--- a/src/startup.publish.ts
+++ b/src/startup.publish.ts
@@ -9,6 +9,7 @@ import { ProseMirrorModule } from "@paperbits/prosemirror/prosemirror.module";
import { StaticSettingsProvider } from "./components/staticSettingsProvider";
import { FileSystemBlobStorage } from "./components/filesystemBlobStorage";
import { ApimPublishModule } from "./apim.publish.module";
+import { PublishingCacheModule } from "./persistence/publishingCacheModule";
/* Reading settings from configuration file */
const configFile = path.resolve(__dirname, "./config.json");
@@ -27,6 +28,7 @@ injector.bindModule(new ProseMirrorModule());
injector.bindModule(new ApimPublishModule());
injector.bindInstance("settingsProvider", settingsProvider);
injector.bindInstance("outputBlobStorage", outputBlobStorage);
+injector.bindModule(new PublishingCacheModule());
injector.resolve("autostart");
/* Allowing self-signed certificates for HTTP requests */