1
0
Форкнуть 0
This commit is contained in:
Ercenk Keresteci 2021-06-30 11:50:46 -07:00
Родитель f1aefa0cb5
Коммит 4a912f4485
272 изменённых файлов: 98901 добавлений и 23245 удалений

Просмотреть файл

@ -46,7 +46,6 @@ In the sections below you will find:
- [Registering Azure Active Directory Applications](#registering-azure-active-directory-applications)
- [Creating a New Directory](#creating-a-new-directory)
- [Registering the Apps](#registering-the-apps)
- [Creating and Configuring a SendGrid Account when using email notifications](#creating-and-configuring-a-sendgrid-account-when-using-email-notifications)
- [Creating a Storage Account](#creating-a-storage-account)
- [Change the Configuration Settings](#change-the-configuration-settings)
- [Create an Offer on Commercial Marketplace Portal in Partner Center](#create-an-offer-on-commercial-marketplace-portal-in-partner-center)
@ -219,13 +218,12 @@ partner center requirements. The rest of the integration is done via notificatio
The landing page can also used for adding new fields to gather more information
from the subscriber; for example: what is the favored region. When a subscriber
provides the details on the landing page, the solution generates a notification to the
configured operations contact. The sample has both the email and Azure storage queue notification implementation. The operations team then provisions the required
configured operations contact. The sample implements a mechanism for Azure storage queue notifications. The operations team then provisions the required
resources, on-boards the customer using their internal processes, and then comes
back to the generated notification and clicks on the link to activate the
back to the generated notification and accesses the URL to activate the
subscription.
Please see my overview for the integration points in
[Integrating a software as a service with Microsoft commercial marketplace](#integrating-a-software-as-a-solution-with-azure-marketplace).
@ -373,14 +371,6 @@ registering two applications:
![A screenshot of a computer Description automatically generated](./ReadmeFiles/AdAppRegistration.png)
### Creating and Configuring a SendGrid Account when using email notifications
Follow the steps in the
[tutorial](https://docs.microsoft.com/en-us/azure/sendgrid-dotnet-how-to-send-email),
and grab an API Key. Set the value of the ApiKey in the configuration section,
"CommandCenter:Mail", either using the user-secrets method or in the
`appconfig.json` file.
### Creating a Storage Account
Create an Azure Storage account following the steps
@ -403,6 +393,7 @@ You will need to replace the values marked as `CHANGE`, either by editing the
| AzureAd:Domain | Change | You can find this value on the "Overview" page of the Active Directory you have registered your applications in. If you are not using a custom domain, it is in the format of \<tenant name\>.onmicrosoft.com |
| AzureAd:TenantId | Keep | Common authentication endpoint, since this is a multi-tenant app |
| AzureAd:ClientId | Change | Copy the clientId of the multi-tenant app from its "Overview" page |
| AzureAd:ClientSecret | Change | Go to the "Certificates & secrets" page of the single-tenant app you have registered, create a new client secret, and copy the value to the clipboard, then set the value for this setting. |
| AzureAd:CallbackPath | Keep | Default oidc sign in path |
| AzureAd:SignedOutCallbackPath | Keep | Default sign out path |
| MarketplaceClient:ClientId | Change | Copy the clientId of the single-tenant app from its "Overview" page. This AD app is for calling the Fulfillment API |
@ -412,14 +403,10 @@ You will need to replace the values marked as `CHANGE`, either by editing the
| WebHookTokenParameters:TenantId | Change | Set the same value as MarketplaceClient:TenantId |
| WebHookTokenParameters:ClientId | Change | Set the same value as MarketplaceClient:ClientId |
| CommandCenter:OperationsStoreConnectionString | Change | Copy the connection string of the storage account you have created in the previous step. Please see [Client library documentation for details](https://github.com/Ercenk/AzureMarketplaceSaaSApiClient#operations-store) |
| CommandCenter:Mail:OperationsTeamEmail | Change | Use this section if ActiveNotificationHandler is EmailNotifications. The sample sends emails to this address. |
| CommandCenter:Mail:FromEmail | Change | Use this section if ActiveNotificationHandler is EmailNotifications. Sendgrid requires a "from" email address when sending emails. |
| CommandCenter:Mail:ApiKey | Change | Use this section if ActiveNotificationHandler is EmailNotifications. Sendgrid API key. |
| CommandCenter:CommandCenterAdmin | Change | Change it to the email address you are logging on to the dashboard. Only the users with the domain name of this email is authorized to use the dashboard to display the subscriptions. |
| CommandCenter:ShowUnsubscribed | Change | Change true or false, depending on if you want to see the subscriptions that are not active. |
| CommandCenter:ActiveNotificationHandler | Change | Active notification handler the solution uses. It can be EmailNotifications or AzureQueueNotifications. |
| CommandCenter:AzureQueue:StorageConnectionString | Change | Use this section if ActiveNotificationHandler is AzureQueueNotifications. Add the storage account connection string for the queue. |
| CommandCenter:AzureQueue:QueueName | Change | Use this section if ActiveNotificationHandler is AzureQueueNotifications. Name of the queue the messages will go to. |
| CommandCenter:AzureQueue:StorageConnectionString | Change | Add the storage account connection string for the queue. |
| CommandCenter:AzureQueue:QueueName | Change | Name of the queue the messages will go to. |
| CommandCenter:EnableDimensionMeterReporting | Change | Use this section to enable manually sending usage events on a dimension for a customer subscription through this App. Default: false, change to true if there is at least one dimension enabled on an Offer-Plan and would like to trigger usage events manually. [More information on Marketplace Metering Service dimensions.](https://docs.microsoft.com/en-us/azure/marketplace/partner-center-portal/saas-metered-billing) |
| CommandCenter:Dimensions:DimensionId | Change | Use this section if the above EnableDimensionMeterReporting setting is true. Add DimensionId of the enabled custom meter. |
| CommandCenter:Dimensions:PlanIds | Change | Use this section if the above EnableDimensionMeterReporting setting is true. Add PlanId's of the plans for which the above DimensionId is enabled. |
@ -661,7 +648,7 @@ Customer searches for the offer on Azure Portal
![purchaser8](./ReadmeFiles/Purchaser8.png)
9. Purchaser submits the form, and Contoso ops team receives an email
9. Purchaser submits the form, and Contoso ops team receives a notification
![purchaser9](./ReadmeFiles/Purchaser9.png)

Двоичные данные
ReadmeFiles/Purchaser10.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 199 KiB

После

Ширина:  |  Высота:  |  Размер: 156 KiB

Двоичные данные
ReadmeFiles/Purchaser9.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 173 KiB

После

Ширина:  |  Высота:  |  Размер: 147 KiB

Просмотреть файл

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "5.0.7",
"commands": [
"dotnet-ef"
]
}
}
}

Просмотреть файл

@ -6,6 +6,7 @@ namespace CommandCenter.Authorization
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using CommandCenter.Utilities;
using Microsoft.AspNetCore.Authorization;
/// <summary>

Просмотреть файл

@ -66,7 +66,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -77,7 +77,7 @@ namespace CommandCenter.AzureQueues
throw new ArgumentNullException(nameof(provisionModel));
}
await this.SendLandingPageMessageAsync(provisionModel, MailLinkControllerName, "Update", cancellationToken).ConfigureAwait(false);
await this.SendLandingPageMessageAsync(provisionModel, MailLinkControllerName, "Update", cancellationToken);
}
/// <inheritdoc/>
@ -90,7 +90,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -101,7 +101,7 @@ namespace CommandCenter.AzureQueues
throw new ArgumentNullException(nameof(provisionModel));
}
await this.SendLandingPageMessageAsync(provisionModel, MailLinkControllerName, "Activate", cancellationToken).ConfigureAwait(false);
await this.SendLandingPageMessageAsync(provisionModel, MailLinkControllerName, "Activate", cancellationToken);
}
/// <inheritdoc/>
@ -114,7 +114,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -127,7 +127,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -140,7 +140,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -153,7 +153,7 @@ namespace CommandCenter.AzureQueues
await this.SendWebhookNotificationMessageAsync(
payload,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
private async Task SendLandingPageMessageAsync(AzureSubscriptionProvisionModel provisionModel, string controllerName, string actionName, CancellationToken cancellationToken)
@ -173,7 +173,7 @@ namespace CommandCenter.AzureQueues
var message = $"{{\"ActionLink\": \"{this.BuildLink(queryParams, controllerName, actionName)}\", \"Payload\" : {JsonConvert.SerializeObject(provisionModel)} }}";
await this.queueClient.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
await this.queueClient.SendMessageAsync(message, cancellationToken);
}
private async Task SendWebhookNotificationMessageAsync(WebhookPayload message, CancellationToken cancellationToken)
@ -183,7 +183,7 @@ namespace CommandCenter.AzureQueues
throw new ArgumentNullException(nameof(message));
}
await this.queueClient.SendMessageAsync(JsonConvert.SerializeObject(message), cancellationToken).ConfigureAwait(false);
await this.queueClient.SendMessageAsync(JsonConvert.SerializeObject(message), cancellationToken);
}
private string BuildLink(List<Tuple<string, string>> queryParams, string controllerName, string controllerAction)

Просмотреть файл

@ -13,6 +13,13 @@
<DocumentationFile></DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Graph\**" />
<Content Remove="Graph\**" />
<EmbeddedResource Remove="Graph\**" />
<None Remove="Graph\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="stylecop.json" />
</ItemGroup>
@ -22,31 +29,36 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Queues" Version="12.6.0" />
<PackageReference Include="Azure.Storage.Queues" Version="12.7.0" />
<PackageReference Include="Ercenk.Microsoft.Marketplace" Version="1.0.0-preview5" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.7" />
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.11.0-1.final" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.8.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.8.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.0.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.14.0" />
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.14.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.14.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
<PackageReference Include="Sendgrid" Version="9.22.0" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.113" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.AzureTableStorage.NETStandard" Version="5.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.354">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\lib\" />
</ItemGroup>
</Project>

Просмотреть файл

@ -5,7 +5,7 @@ namespace CommandCenter
{
using System;
using System.Collections.Generic;
using CommandCenter.Mail;
using CommandCenter.Metering;
/// <summary>
/// Options for the command center.
@ -22,11 +22,6 @@ namespace CommandCenter
/// </summary>
public string CommandCenterAdmin { get; set; }
/// <summary>
/// Gets or sets command center's email options.
/// </summary>
public MailOptions Mail { get; set; }
/// <summary>
/// Gets or sets a value indicating whether unsubcribed subscriptions are shown.
/// </summary>
@ -42,8 +37,8 @@ namespace CommandCenter
/// </summary>
public AzureQueueOptions AzureQueue { get; set; }
/// <summary>
/// Gets or sets the UseMeteredDimensions.
/// <summary>
/// Gets or sets a value indicating whether meter reporting is enabled.
/// </summary>
public bool EnableDimensionMeterReporting { get; set; }
@ -51,10 +46,5 @@ namespace CommandCenter
/// Gets or sets the Dimensions.
/// </summary>
public List<Dimension> Dimensions { get; set; }
/// <summary>
/// Gets or sets the active notification handler.
/// </summary>
public NotificationHandlerEnum ActiveNotificationHandler { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter.Controllers
{
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Resolving redirect after signout.
/// </summary>
[AllowAnonymous]
public class AccountController : Controller
{
/// <summary>
/// Redirects to home page after sign out.
/// </summary>
/// <param name="page">Page.</param>
/// <returns>IActionResult.</returns>
[HttpGet]
public IActionResult SignOut(string page)
{
return this.RedirectToAction("Index", "Subscriptions");
}
}
}

Просмотреть файл

@ -4,6 +4,7 @@
namespace CommandCenter.Controllers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
@ -16,21 +17,22 @@ namespace CommandCenter.Controllers
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Graph;
using Microsoft.Identity.Web;
using Microsoft.Marketplace.SaaS;
using Microsoft.Marketplace.SaaS.Models;
/// <summary>
/// Landing page.
/// </summary>
[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)]
// Specify the auth scheme to be used for logging on users. This is for supporting WebAPI auth
[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] // Specify the auth scheme to be used for logging on users. This is for supporting WebAPI auth
public class LandingPageController : Controller
{
private readonly ILogger<LandingPageController> logger;
private readonly IMarketplaceProcessor marketplaceProcessor;
private readonly IMarketplaceNotificationHandler notificationHandler;
private readonly IMarketplaceSaaSClient marketplaceClient;
private readonly GraphServiceClient graphServiceClient;
private readonly CommandCenterOptions options;
/// <summary>
@ -40,12 +42,14 @@ namespace CommandCenter.Controllers
/// <param name="marketplaceProcessor">Marketplace processor.</param>
/// <param name="notificationHandler">Notification handler.</param>
/// <param name="marketplaceClient">Marketplace client.</param>
/// <param name="graphServiceClient">Client for Graph API.</param>
/// <param name="logger">Logger.</param>
public LandingPageController(
IOptionsMonitor<CommandCenterOptions> commandCenterOptions,
IMarketplaceProcessor marketplaceProcessor,
IMarketplaceNotificationHandler notificationHandler,
IMarketplaceSaaSClient marketplaceClient,
GraphServiceClient graphServiceClient,
ILogger<LandingPageController> logger)
{
if (commandCenterOptions == null)
@ -56,6 +60,7 @@ namespace CommandCenter.Controllers
this.marketplaceProcessor = marketplaceProcessor;
this.notificationHandler = notificationHandler;
this.marketplaceClient = marketplaceClient;
this.graphServiceClient = graphServiceClient;
this.logger = logger;
this.options = commandCenterOptions.CurrentValue;
}
@ -66,6 +71,7 @@ namespace CommandCenter.Controllers
/// <param name="token">Marketplace purchase identification token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Action result.</returns>
[AuthorizeForScopes(Scopes = new string[] { "user.read" })]
public async Task<ActionResult> Index(string token, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(token))
@ -75,59 +81,77 @@ namespace CommandCenter.Controllers
return this.View();
}
// Get the subscription for the offer from the marketplace purchase identification token
var resolvedSubscription = await this.marketplaceProcessor.GetSubscriptionFromPurchaseIdentificationTokenAsync(token, cancellationToken).ConfigureAwait(false);
ResolvedSubscription resolvedSubscription = null;
Microsoft.Marketplace.SaaS.Models.Subscription subscriptionDetails = null;
Azure.Response<Microsoft.Marketplace.SaaS.Models.SubscriptionPlans> availablePlans = null;
bool anyPendingOperations = false;
// Rest is implementation detail. In this sample, we chose allow the subscriber to change the plan for an activated subscriptio
if (resolvedSubscription == default(ResolvedSubscription))
if (token.ToLowerInvariant() != "sampletoken")
{
this.ViewBag.Message = "Token did not resolve to a subscription";
return this.View();
// Get the subscription for the offer from the marketplace purchase identification token
resolvedSubscription = await this.marketplaceProcessor.GetSubscriptionFromPurchaseIdentificationTokenAsync(token, cancellationToken);
if (resolvedSubscription == default(ResolvedSubscription))
{
this.ViewBag.Message = "Token did not resolve to a subscription";
return this.View();
}
subscriptionDetails = resolvedSubscription.Subscription;
// Populate the available plans for this subscription from the API
availablePlans = await this.marketplaceClient.Fulfillment.ListAvailablePlansAsync(
resolvedSubscription.Id.Value,
null,
null,
cancellationToken);
// See if there are pending operations for this subscription
var pendingOperations = await this.marketplaceClient.Operations.ListOperationsAsync(
resolvedSubscription.Id.Value,
null,
null,
cancellationToken);
anyPendingOperations = pendingOperations?.Value.Operations?.Any(o => o.Status == OperationStatusEnum.InProgress) ?? false;
}
// resolvedSubscription.Subscription is null when calling mock endpoint
var existingSubscription = resolvedSubscription.Subscription;
var availablePlans = await this.marketplaceClient.Fulfillment.ListAvailablePlansAsync(
resolvedSubscription.Id.Value,
null,
null,
cancellationToken).ConfigureAwait(false);
var pendingOperations = await this.marketplaceClient.Operations.ListOperationsAsync(
resolvedSubscription.Id.Value,
null,
null,
cancellationToken).ConfigureAwait(false);
var graphApiUser = await this.graphServiceClient.Me.Request().GetAsync();
var provisioningModel = new AzureSubscriptionProvisionModel
{
PlanId = resolvedSubscription.PlanId,
SubscriptionId = resolvedSubscription.Id.Value,
OfferId = resolvedSubscription.OfferId,
SubscriptionName = resolvedSubscription.SubscriptionName,
PurchaserEmail = existingSubscription?.Purchaser?.EmailId,
PurchaserTenantId = existingSubscription?.Purchaser?.TenantId ?? Guid.Empty,
// Landing page is the only place to capture the customer's contact details
// It can be present in multiple places:
// - the details received from the Graph API
// - beneficiary information on the subscription details
// it is also possible that the Graph API
NameFromOpenIdConnect = (this.User.Identity as ClaimsIdentity)?.FindFirst("name")?.Value,
EmailFromClaims = this.User.Identity.GetUserEmail(),
EmailFromGraph = graphApiUser.Mail ?? string.Empty,
NameFromGraph = graphApiUser.DisplayName ?? string.Empty,
UserPrincipalName = graphApiUser.UserPrincipalName ?? string.Empty,
PurchaserEmail = graphApiUser.Mail ?? string.Empty,
// Get the other potential contact information from the marketplace API
PurchaserUPN = token.ToLowerInvariant() == "sampletoken" ? "purchaser@purchaser.com" : subscriptionDetails?.Purchaser?.EmailId,
PurchaserTenantId = token.ToLowerInvariant() == "sampletoken" ? Guid.Empty : subscriptionDetails?.Purchaser?.TenantId ?? Guid.Empty,
BeneficiaryUPN = token.ToLowerInvariant() == "sampletoken" ? "customer@customer.com" : subscriptionDetails?.Beneficiary?.EmailId,
BeneficiaryTenantId = token.ToLowerInvariant() == "sampletoken" ? Guid.Empty : subscriptionDetails?.Beneficiary?.TenantId ?? Guid.Empty,
// Maybe the end users are a completely different set of contacts, start with one
BusinessUnitContactEmail = this.User.Identity.GetUserEmail(),
PlanId = token.ToLowerInvariant() == "sampletoken" ? "purchaser@purchaser.com" : resolvedSubscription.PlanId,
SubscriptionId = token.ToLowerInvariant() == "sampletoken" ? Guid.Empty : resolvedSubscription.Id.Value,
OfferId = token.ToLowerInvariant() == "sampletoken" ? "sample offer" : resolvedSubscription.OfferId,
SubscriptionName = token.ToLowerInvariant() == "sampletoken" ? "sample subscription" : resolvedSubscription.SubscriptionName,
SubscriptionStatus = token.ToLowerInvariant() == "sampletoken" ? SubscriptionStatusEnum.PendingFulfillmentStart : subscriptionDetails?.SaasSubscriptionStatus ?? SubscriptionStatusEnum.NotStarted,
// Assuming this will be set to the value the customer already set when subscribing, if we are here after the initial subscription activation
// Landing page is used both for initial provisioning and configuration of the subscription.
Region = TargetContosoRegionEnum.NorthAmerica,
AvailablePlans = availablePlans?.Value.Plans.ToList(),
SubscriptionStatus = existingSubscription?.SaasSubscriptionStatus ?? SubscriptionStatusEnum.NotStarted,
PendingOperations = pendingOperations?.Value.Operations?.Any(o => o.Status == OperationStatusEnum.InProgress) ?? false,
AvailablePlans = token.ToLowerInvariant() == "sampletoken" ? new System.Collections.Generic.List<Plan>() : availablePlans?.Value.Plans.ToList(),
PendingOperations = token.ToLowerInvariant() == "sampletoken" ? false : anyPendingOperations,
};
if (provisioningModel != default)
{
provisioningModel.FullName = (this.User.Identity as ClaimsIdentity)?.FindFirst("name")?.Value;
provisioningModel.Email = this.User.Identity.GetUserEmail();
provisioningModel.BusinessUnitContactEmail = this.User.Identity.GetUserEmail();
return this.View(provisioningModel);
}
this.ModelState.AddModelError(string.Empty, "Cannot resolve subscription");
return this.View();
return this.View(provisioningModel);
}
/// <summary>
@ -152,11 +176,11 @@ namespace CommandCenter.Controllers
// A new subscription will have PendingFulfillmentStart as status
if (provisionModel.SubscriptionStatus == SubscriptionStatusEnum.PendingFulfillmentStart)
{
await this.notificationHandler.ProcessNewSubscriptionAsyc(provisionModel, cancellationToken).ConfigureAwait(false);
await this.notificationHandler.ProcessNewSubscriptionAsyc(provisionModel, cancellationToken);
}
else
{
await this.notificationHandler.ProcessChangePlanAsync(provisionModel, cancellationToken).ConfigureAwait(false);
await this.notificationHandler.ProcessChangePlanAsync(provisionModel, cancellationToken);
}
return this.RedirectToAction(nameof(this.Success));

Просмотреть файл

@ -48,7 +48,7 @@ namespace CommandCenter.Controllers
throw new ArgumentNullException(nameof(notificationModel));
}
var subscriptionDetails = (await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(notificationModel.SubscriptionId, null, null, cancellationToken).ConfigureAwait(false)).Value;
var subscriptionDetails = (await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(notificationModel.SubscriptionId, null, null, cancellationToken)).Value;
if (subscriptionDetails.SaasSubscriptionStatus != Microsoft.Marketplace.SaaS.Models.SubscriptionStatusEnum.PendingFulfillmentStart)
{
@ -61,7 +61,7 @@ namespace CommandCenter.Controllers
});
}
await this.marketplaceProcessor.ActivateSubscriptionAsync(notificationModel.SubscriptionId, notificationModel.PlanId, cancellationToken).ConfigureAwait(false);
await this.marketplaceProcessor.ActivateSubscriptionAsync(notificationModel.SubscriptionId, notificationModel.PlanId, cancellationToken);
return this.View(
new ActivateActionViewModel
@ -88,7 +88,7 @@ namespace CommandCenter.Controllers
throw new ArgumentNullException(nameof(notificationModel));
}
await this.OperationAckAsync(notificationModel, cancellationToken).ConfigureAwait(false);
await this.OperationAckAsync(notificationModel, cancellationToken);
return this.View("OperationUpdate", notificationModel);
}
@ -109,7 +109,7 @@ namespace CommandCenter.Controllers
throw new ArgumentNullException(nameof(notificationModel));
}
await this.OperationAckAsync(notificationModel, cancellationToken).ConfigureAwait(false);
await this.OperationAckAsync(notificationModel, cancellationToken);
return this.View("OperationUpdate", notificationModel);
}
@ -130,7 +130,7 @@ namespace CommandCenter.Controllers
throw new ArgumentNullException(nameof(notificationModel));
}
await this.OperationAckAsync(notificationModel, cancellationToken).ConfigureAwait(false);
await this.OperationAckAsync(notificationModel, cancellationToken);
return this.View("OperationUpdate", notificationModel);
}
@ -151,7 +151,7 @@ namespace CommandCenter.Controllers
throw new ArgumentNullException(nameof(notificationModel));
}
await this.OperationAckAsync(notificationModel, cancellationToken).ConfigureAwait(false);
await this.OperationAckAsync(notificationModel, cancellationToken);
return this.View("OperationUpdate", notificationModel);
}
@ -175,7 +175,7 @@ namespace CommandCenter.Controllers
new Microsoft.Marketplace.SaaS.Models.SubscriberPlan { PlanId = notificationModel.PlanId },
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
return this.View(
new ActivateActionViewModel
@ -189,7 +189,7 @@ namespace CommandCenter.Controllers
NotificationModel payload,
CancellationToken cancellationToken)
{
await this.marketplaceProcessor.OperationAckAsync(payload.SubscriptionId, payload.OperationId, payload.PlanId, payload.Quantity, cancellationToken).ConfigureAwait(false);
await this.marketplaceProcessor.OperationAckAsync(payload.SubscriptionId, payload.OperationId, payload.PlanId, payload.Quantity, cancellationToken);
}
}
}

Просмотреть файл

@ -10,6 +10,7 @@ namespace CommandCenter.Controllers
using System.Threading;
using System.Threading.Tasks;
using CommandCenter.DimensionUsageStore;
using CommandCenter.Metering;
using CommandCenter.Models;
using CommandCenter.OperationsStore;
@ -90,7 +91,7 @@ namespace CommandCenter.Controllers
{
try
{
var subscriptions = await marketplaceClient.Fulfillment.ListSubscriptionsAsync(cancellationToken: cancellationToken).ToListAsync();
var subscriptions = await this.marketplaceClient.Fulfillment.ListSubscriptionsAsync(cancellationToken: cancellationToken).ToListAsync();
var subscriptionsViewModel = subscriptions.Select(SubscriptionViewModel.FromSubscription)
.Where(s => s.State != SubscriptionStatusEnum.Unsubscribed || this.options.ShowUnsubscribed);
@ -106,7 +107,7 @@ namespace CommandCenter.Controllers
foreach (var task in taskList)
{
var subscription = await task.ConfigureAwait(false);
var subscription = await task;
newViewModel.Add(subscription);
}
@ -114,7 +115,7 @@ namespace CommandCenter.Controllers
}
catch (Exception ex)
{
this.ModelState.AddModelError(string.Empty, "Something went wrong, please check logs!");
this.ModelState.AddModelError(string.Empty, $"Something went wrong, please check logs, but here some details {ex.ToString()}");
return this.View(new List<SubscriptionViewModel>());
}
}
@ -139,10 +140,10 @@ namespace CommandCenter.Controllers
public async Task<IActionResult> Operations(Guid subscriptionId, CancellationToken cancellationToken)
{
var subscriptionOperations =
await this.operationsStore.GetAllSubscriptionRecordsAsync(subscriptionId, cancellationToken).ConfigureAwait(false);
await this.operationsStore.GetAllSubscriptionRecordsAsync(subscriptionId, cancellationToken);
var subscription =
(await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(subscriptionId, null, null, cancellationToken).ConfigureAwait(false)).Value;
(await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(subscriptionId, null, null, cancellationToken)).Value;
var operations = new List<Operation>();
@ -154,7 +155,7 @@ namespace CommandCenter.Controllers
operation.OperationId,
null,
null,
cancellationToken).ConfigureAwait(false));
cancellationToken));
}
return this.View(new OperationsViewModel
@ -175,7 +176,7 @@ namespace CommandCenter.Controllers
public async Task<IActionResult> SubscriptionDimensionUsage(Guid subscriptionId, bool showErrorMessage, CancellationToken cancellationToken)
{
var subscription =
(await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(subscriptionId, null, null, cancellationToken).ConfigureAwait(false)).Value;
(await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(subscriptionId, null, null, cancellationToken)).Value;
var dimensionEventViewModel = new DimensionEventViewModel()
{
@ -186,7 +187,7 @@ namespace CommandCenter.Controllers
SubscriptionDimensions = this.options.Dimensions
.Where(dim => dim.PlanIds.Contains(subscription.PlanId) && dim.OfferIds.Contains(subscription.OfferId))
.Select(dim => dim.DimensionId).ToList(),
PastUsageEvents = await this.dimensionUsageStore.GetAllDimensionRecordsAsync(subscriptionId, cancellationToken).ConfigureAwait(false),
PastUsageEvents = await this.dimensionUsageStore.GetAllDimensionRecordsAsync(subscriptionId, cancellationToken),
};
if (showErrorMessage)
@ -220,7 +221,7 @@ namespace CommandCenter.Controllers
EffectiveStartTime = model.EventTime,
};
var updateResult = (await this.meteringClient.Metering.PostUsageEventAsync(usage, null, null, cancellationToken).ConfigureAwait(false)).Value;
var updateResult = (await this.meteringClient.Metering.PostUsageEventAsync(usage, null, null, cancellationToken)).Value;
DimensionUsageRecord dimRecord = new DimensionUsageRecord(usage.ResourceId?.ToString(), DateTime.Now.ToString("o"));
bool errorMessage = true;
@ -244,7 +245,7 @@ namespace CommandCenter.Controllers
dimRecord.PlanId = usage.PlanId;
}
await this.dimensionUsageStore.RecordAsync(model.SubscriptionId, dimRecord, cancellationToken).ConfigureAwait(false);
await this.dimensionUsageStore.RecordAsync(model.SubscriptionId, dimRecord, cancellationToken);
return this.RedirectToAction("SubscriptionDimensionUsage", new { model.SubscriptionId, errorMessage });
}
@ -271,19 +272,19 @@ namespace CommandCenter.Controllers
subscriptionId,
null,
null,
cancellationToken).ConfigureAwait(false)).Value;
cancellationToken)).Value;
var subscription = (await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(
subscriptionId,
null,
null,
cancellationToken).ConfigureAwait(false)).Value;
cancellationToken)).Value;
var pendingOperations = (await this.marketplaceClient.Operations.ListOperationsAsync(
subscriptionId,
null,
null,
cancellationToken).ConfigureAwait(false)).Value;
cancellationToken)).Value;
var updateSubscriptionViewModel = new UpdateSubscriptionViewModel
{
@ -305,7 +306,7 @@ namespace CommandCenter.Controllers
subscriptionId,
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
return this.RedirectToAction("Index");
@ -336,7 +337,7 @@ namespace CommandCenter.Controllers
model.SubscriptionId,
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
if (pendingOperations.Value.Operations.Any(o => o.Status == OperationStatusEnum.InProgress))
{
@ -348,9 +349,9 @@ namespace CommandCenter.Controllers
new SubscriberPlan { PlanId = model.NewPlan },
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
await this.operationsStore.RecordAsync(model.SubscriptionId, Guid.Parse(updateResult), cancellationToken).ConfigureAwait(false);
await this.operationsStore.RecordAsync(model.SubscriptionId, Guid.Parse(updateResult), cancellationToken);
return this.RedirectToAction("Index");
}
@ -360,11 +361,11 @@ namespace CommandCenter.Controllers
var recordedSubscriptionOperations =
await this.operationsStore.GetAllSubscriptionRecordsAsync(
subscription.SubscriptionId,
cancellationToken).ConfigureAwait(false);
cancellationToken);
subscription.ExistingOperations = (await this.operationsStore.GetAllSubscriptionRecordsAsync(
subscription.SubscriptionId,
cancellationToken).ConfigureAwait(false)).Any();
cancellationToken)).Any();
subscription.OperationCount = recordedSubscriptionOperations.Count();
if (this.options.EnableDimensionMeterReporting)

Просмотреть файл

@ -19,9 +19,7 @@ namespace CommandCenter.Controllers
/// <summary>
/// Webhook controller.
/// </summary>
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
// Specify the auth scheme to be used for logging on users. This is for supporting WebAPI auth
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // Specify the auth scheme to be used for logging on users. This is for supporting WebAPI auth
[RequireHttps]
[Route("api/[controller]")]
[ApiController]
@ -65,11 +63,11 @@ namespace CommandCenter.Controllers
var payload = string.Empty;
using (var reader = new StreamReader(this.Request.Body, Encoding.UTF8))
{
payload = await reader.ReadToEndAsync().ConfigureAwait(false);
payload = await reader.ReadToEndAsync();
this.logger.LogInformation($"{payload}");
}
await this.marketplaceProcessor.ProcessWebhookNotificationAsync(JsonConvert.DeserializeObject<WebhookPayload>(payload), CancellationToken.None).ConfigureAwait(false);
await this.marketplaceProcessor.ProcessWebhookNotificationAsync(JsonConvert.DeserializeObject<WebhookPayload>(payload), CancellationToken.None);
return this.Ok();
}

Просмотреть файл

@ -1,335 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter.Mail
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommandCenter.Marketplace;
using CommandCenter.Models;
using CommandCenter.Utilities;
using Microsoft.Extensions.Options;
using Microsoft.Marketplace.SaaS;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SendGrid;
using SendGrid.Helpers.Mail;
/// <summary>
/// Helper for email messages.
/// </summary>
public class CommandCenterEMailHelper : IMarketplaceNotificationHandler
{
private const string MailLinkControllerName = "MailLink";
private readonly IMarketplaceSaaSClient marketplaceClient;
private readonly CommandCenterOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="CommandCenterEMailHelper"/> class.
/// </summary>
/// <param name="optionsMonitor">Options monitor.</param>
/// <param name="marketplaceClient">Marketplace API client.</param>
public CommandCenterEMailHelper(
IOptionsMonitor<CommandCenterOptions> optionsMonitor,
IMarketplaceSaaSClient marketplaceClient)
{
if (optionsMonitor == null)
{
throw new ArgumentNullException(nameof(optionsMonitor));
}
this.marketplaceClient = marketplaceClient;
this.options = optionsMonitor.CurrentValue;
}
/// <inheritdoc/>
public async Task NotifyChangePlanAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
await this.SendWebhookNotificationEmailAsync(
"Plan change request complete",
$"Plan change request complete. Please take the required action.",
string.Empty,
payload,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ProcessNewSubscriptionAsyc(
AzureSubscriptionProvisionModel provisionModel,
CancellationToken cancellationToken = default)
{
if (provisionModel == null)
{
throw new ArgumentNullException(nameof(provisionModel));
}
var queryParams = new List<Tuple<string, string>>
{
new Tuple<string, string>(
"subscriptionId",
provisionModel.SubscriptionId.ToString()),
new Tuple<string, string>("planId", provisionModel.PlanId),
};
var emailText =
"<p>New subscription. Please take the required action, then return to this email and click the following link to confirm. ";
emailText += $"{this.BuildALink("Activate", queryParams, "Click here to activate subscription")}.</p>";
emailText +=
$"<div> <p> Details are</p> <div> {BuildTable(JObject.Parse(JsonConvert.SerializeObject(provisionModel)))}</div></div>";
await this.SendEmailAsync(
() => $"New subscription, {provisionModel.SubscriptionName}",
() => emailText,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ProcessChangePlanAsync(
AzureSubscriptionProvisionModel provisionModel,
CancellationToken cancellationToken = default)
{
if (provisionModel == null)
{
throw new ArgumentNullException(nameof(provisionModel));
}
var queryParams = new List<Tuple<string, string>>
{
new Tuple<string, string>(
"subscriptionId",
provisionModel.SubscriptionId.ToString()),
new Tuple<string, string>("planId", provisionModel.NewPlanId),
};
var emailText = $"<p>Updated subscription from {provisionModel.PlanId} to {provisionModel.NewPlanId}.";
emailText +=
"Please take the required action, then return to this email and click the following link to confirm. ";
emailText += $"{this.BuildALink("Update", queryParams, "Click here to update subscription")}.</p>";
emailText +=
$"<div> <p> Details are</p> <div> {BuildTable(JObject.Parse(JsonConvert.SerializeObject(provisionModel)))}</div></div>";
await this.SendEmailAsync(
() => $"Update subscription, {provisionModel.SubscriptionName}",
() => emailText,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task NotifyChangeQuantityAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
await this.SendWebhookNotificationEmailAsync(
"Quantity change request",
"Quantity change request. Please take the required action.",
string.Empty,
payload,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ProcessOperationFailOrConflictAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
var notificationModel = NotificationModel.FromWebhookPayload(payload);
var queryParams = new List<Tuple<string, string>>
{
new Tuple<string, string>(
"subscriptionId",
notificationModel.SubscriptionId.ToString()),
};
var subscriptionDetails = await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(
notificationModel.SubscriptionId,
Guid.Empty,
Guid.Empty,
cancellationToken).ConfigureAwait(false);
await this.SendEmailAsync(
() => $"Operation failure, {subscriptionDetails.Value.Name}",
() =>
$"<p>Operation failure. {this.BuildALink("Operations", queryParams, "Click here to list all operations for this subscription", "Subscriptions")}</p>. "
+ $"<p> Details are {BuildTable(JObject.Parse(JsonConvert.SerializeObject(subscriptionDetails)))}</p>",
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task NotifyReinstatedAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
await this.SendWebhookNotificationEmailAsync(
"Reinstate subscription request",
"Reinstate subscription request. Please take the required action, then return to this email and click the following link to confirm.",
"Reinstate",
payload,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task NotifySuspendedAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
await this.SendWebhookNotificationEmailAsync(
"Suspend subscription request",
"Suspend subscription request. Please take the required action.",
string.Empty,
payload,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task NotifyUnsubscribedAsync(
WebhookPayload payload,
CancellationToken cancellationToken = default)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
await this.SendWebhookNotificationEmailAsync(
"Cancel subscription request",
"Cancel subscription request. Please take the required action.",
string.Empty,
payload,
cancellationToken).ConfigureAwait(false);
}
private static string BuildTable(JObject parsed)
{
var tableContents = parsed.Properties().AsEnumerable()
.Select(p => $"<tr><th align=\"left\"> {p.Name} </th><th align=\"left\"> {p.Value}</th></tr>")
.Aggregate((head, tail) => head + tail);
return $"<table border=\"1\" align=\"left\">{tableContents}</table>";
}
private string BuildALink(
string controllerAction,
IEnumerable<Tuple<string, string>> queryParams,
string innerText,
string controllerName = MailLinkControllerName)
{
var uriStart = FluentUriBuilder.Start(this.options.BaseUrl).AddPath(controllerName)
.AddPath(controllerAction);
foreach (var (item1, item2) in queryParams) uriStart.AddQuery(item1, item2);
var href = uriStart.Uri.ToString();
return $"<a href=\"{href}\">{innerText}</a>";
}
private async Task SendEmailAsync(
Func<string> subjectBuilder,
Func<string> contentBuilder,
CancellationToken cancellationToken = default)
{
var msg = new SendGridMessage();
msg.SetFrom(new EmailAddress(this.options.Mail.FromEmail, "Marketplace command center"));
var recipients = new List<EmailAddress> { new EmailAddress(this.options.Mail.OperationsTeamEmail) };
msg.AddTos(recipients);
msg.SetSubject(subjectBuilder());
msg.AddContent(MimeType.Html, contentBuilder());
var client = new SendGridClient(this.options.Mail.ApiKey);
var response = await client.SendEmailAsync(msg, cancellationToken).ConfigureAwait(false);
if ((response.StatusCode != System.Net.HttpStatusCode.OK) &&
(response.StatusCode != System.Net.HttpStatusCode.Accepted))
{
throw new ApplicationException(await response.Body.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
}
}
private async Task SendWebhookNotificationEmailAsync(
string subject,
string mailBody,
string actionName,
WebhookPayload payload,
CancellationToken cancellationToken)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
var notificationModel = NotificationModel.FromWebhookPayload(payload);
if (string.IsNullOrEmpty(actionName))
{
var queryParams = new List<Tuple<string, string>>
{
new Tuple<string, string>(
"subscriptionId",
notificationModel.SubscriptionId.ToString()),
new Tuple<string, string>("publisherId", notificationModel.PublisherId),
new Tuple<string, string>("offerId", notificationModel.OfferId),
new Tuple<string, string>("planId", notificationModel.PlanId),
new Tuple<string, string>("quantity", notificationModel.Quantity.ToString(CultureInfo.InvariantCulture)),
new Tuple<string, string>("operationId", notificationModel.OperationId.ToString()),
};
var actionLink = !string.IsNullOrEmpty(actionName)
? this.BuildALink(actionName, queryParams, "Click here to confirm.")
: string.Empty;
mailBody = $"{mailBody}" + $"{actionLink}";
}
var subscriptionDetails = await this.marketplaceClient.Fulfillment.GetSubscriptionAsync(
notificationModel.SubscriptionId,
Guid.Empty,
Guid.Empty,
cancellationToken).ConfigureAwait(false);
await this.SendEmailAsync(
() => $"{subject}, {subscriptionDetails.Value.Name}",
() => $"<p>{mailBody}</p>"
+ $"<br/><div> Details are {BuildTable(JObject.Parse(JsonConvert.SerializeObject(subscriptionDetails)))}</div>",
cancellationToken).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -1,26 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter.Mail
{
/// <summary>
/// Email related options.
/// </summary>
public class MailOptions
{
/// <summary>
/// Gets or sets operations team email.
/// </summary>
public string OperationsTeamEmail { get; set; }
/// <summary>
/// Gets or sets SendGrid API key.
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// Gets or sets from email address.
/// </summary>
public string FromEmail { get; set; }
}
}

Просмотреть файл

@ -46,7 +46,7 @@ namespace CommandCenter.Marketplace
new SubscriberPlan { PlanId = planId },
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
this.logger.LogInformation($"Activated subscription {subscriptionId} with plan {planId}");
}
@ -61,7 +61,7 @@ namespace CommandCenter.Marketplace
try
{
var resolvedSubscription = await this.marketplaceClient.Fulfillment.ResolveAsync(token, null, null, cancellationToken).ConfigureAwait(false);
var resolvedSubscription = await this.marketplaceClient.Fulfillment.ResolveAsync(token, null, null, cancellationToken);
if (resolvedSubscription != default)
{
@ -88,7 +88,7 @@ namespace CommandCenter.Marketplace
new UpdateOperation { PlanId = planId, Quantity = quantity, Status = UpdateOperationStatusEnum.Success },
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
/// <inheritdoc/>
@ -105,7 +105,7 @@ namespace CommandCenter.Marketplace
payload.OperationId,
null,
null,
cancellationToken).ConfigureAwait(false);
cancellationToken);
if (operationDetails == null)
{
@ -119,23 +119,23 @@ namespace CommandCenter.Marketplace
switch (payload.Action)
{
case WebhookAction.Unsubscribe:
await this.webhookHandler.UnsubscribedAsync(payload).ConfigureAwait(false);
await this.webhookHandler.UnsubscribedAsync(payload);
break;
case WebhookAction.ChangePlan:
await this.webhookHandler.ChangePlanAsync(payload).ConfigureAwait(false);
await this.webhookHandler.ChangePlanAsync(payload);
break;
case WebhookAction.ChangeQuantity:
await this.webhookHandler.ChangeQuantityAsync(payload).ConfigureAwait(false);
await this.webhookHandler.ChangeQuantityAsync(payload);
break;
case WebhookAction.Suspend:
await this.webhookHandler.SuspendedAsync(payload).ConfigureAwait(false);
await this.webhookHandler.SuspendedAsync(payload);
break;
case WebhookAction.Reinstate:
await this.webhookHandler.ReinstatedAsync(payload).ConfigureAwait(false);
await this.webhookHandler.ReinstatedAsync(payload);
break;
case WebhookAction.Transfer:

Просмотреть файл

@ -9,27 +9,27 @@ namespace CommandCenter.Marketplace
public enum WebhookAction
{
/// <summary>
/// (When the resource has been deleted)
/// When the resource has been deleted.
/// </summary>
Unsubscribe,
/// <summary>
/// (When the change plan operation has completed)
/// When the change plan operation has completed.
/// </summary>
ChangePlan,
/// <summary>
/// (When the change quantity operation has completed),
/// When the change quantity operation has completed.
/// </summary>
ChangeQuantity,
/// <summary>
/// (When resource has been suspended)
/// When resource has been suspended.
/// </summary>
Suspend,
/// <summary>
/// (When resource has been reinstated after suspension)
/// When resource has been reinstated after suspension.
/// </summary>
Reinstate,

Просмотреть файл

@ -1,13 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter.DimensionUsageStore
namespace CommandCenter.Metering
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommandCenter.DimensionUsageStore;
using Microsoft.Azure.Cosmos.Table;
/// <summary>
@ -25,13 +26,13 @@ namespace CommandCenter.DimensionUsageStore
/// <param name="storageAccountConnectionString">Storage account connection string.</param>
public AzureTableDimensionUsageStore(string storageAccountConnectionString)
{
this.tableClient = CloudStorageAccount.Parse(storageAccountConnectionString).CreateCloudTableClient();
tableClient = CloudStorageAccount.Parse(storageAccountConnectionString).CreateCloudTableClient();
}
/// <inheritdoc/>
public async Task<IEnumerable<DimensionUsageRecord>> GetAllDimensionRecordsAsync(Guid subscriptionId, CancellationToken cancellationToken = default)
{
var table = this.tableClient.GetTableReference(TableName);
var table = tableClient.GetTableReference(TableName);
var result = new List<DimensionUsageRecord>();
if (!table.Exists())
@ -56,7 +57,7 @@ namespace CommandCenter.DimensionUsageStore
EffectiveStartTime = DateTime.Parse(properties["EffectiveStartTime"]?.ToString()),
},
token,
cancellationToken).ConfigureAwait(false);
cancellationToken);
result.AddRange(segment.Results.Select(r => r));
@ -73,23 +74,23 @@ namespace CommandCenter.DimensionUsageStore
DimensionUsageRecord result,
CancellationToken cancellationToken = default)
{
var table = this.tableClient.GetTableReference(TableName);
var table = tableClient.GetTableReference(TableName);
await this.InitTable(cancellationToken).ConfigureAwait(false);
await InitTable(cancellationToken);
var tableOperation = TableOperation.InsertOrMerge(result);
await table.ExecuteAsync(tableOperation, cancellationToken).ConfigureAwait(false);
await table.ExecuteAsync(tableOperation, cancellationToken);
}
private async Task InitTable(CancellationToken cancellationToken = default)
{
if (!this.tableInitialized)
if (!tableInitialized)
{
var table = this.tableClient.GetTableReference(TableName);
var table = tableClient.GetTableReference(TableName);
await table.CreateIfNotExistsAsync(cancellationToken).ConfigureAwait(false);
this.tableInitialized = true;
await table.CreateIfNotExistsAsync(cancellationToken);
tableInitialized = true;
}
}
}

Просмотреть файл

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter
namespace CommandCenter.Metering
{
using System;
using System.Collections.Generic;

Просмотреть файл

@ -1,12 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter.DimensionUsageStore
namespace CommandCenter.Metering
{
using System;
using System.Net;
using Microsoft.Azure.Cosmos.Table;
using Microsoft.Marketplace.Metering.Models;
/// <summary>
/// Dimension record entity.
@ -20,14 +18,14 @@ namespace CommandCenter.DimensionUsageStore
/// <param name="sentDateTime">Sent Datetime as the row key.</param>
public DimensionUsageRecord(string subscriptionId, string sentDateTime)
{
this.PartitionKey = subscriptionId;
this.RowKey = sentDateTime;
PartitionKey = subscriptionId;
RowKey = sentDateTime;
}
/// <summary>
/// Gets or sets unique identifier associated with the usage event.
/// </summary>
public System.Guid? UsageEventId { get; set; }
public Guid? UsageEventId { get; set; }
/// <summary>
/// Gets or sets status of the operation. Possible values include:
@ -49,7 +47,7 @@ namespace CommandCenter.DimensionUsageStore
/// <summary>
/// Gets or sets time in UTC when the usage event occurred.
/// </summary>
public System.DateTimeOffset? EffectiveStartTime { get; set; }
public DateTimeOffset? EffectiveStartTime { get; set; }
/// <summary>
/// Gets or sets plan associated with the purchased offer.

Просмотреть файл

@ -7,6 +7,7 @@ namespace CommandCenter.DimensionUsageStore
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommandCenter.Metering;
/// <summary>
/// Interface for storing triggered dimension usage events.

Просмотреть файл

@ -4,6 +4,7 @@
namespace CommandCenter.Models
{
using System;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Activate action view model.
@ -13,12 +14,19 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets plan ID.
/// </summary>
[Display(Name = "Plan ID")]
public string PlanId { get; set; }
/// <summary>
/// Gets or sets subscription ID.
/// </summary>
[Display(Name = "Subscription ID")]
public Guid SubscriptionId { get; set; }
/// <summary>
/// Gets message.
/// </summary>
[Display(Name = "Message")]
public string Message { get; internal set; }
}
}

Просмотреть файл

@ -32,13 +32,14 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets email.
/// </summary>
public string Email { get; set; }
[Display(Name = "Preferred user name")]
public string EmailFromClaims { get; set; }
/// <summary>
/// Gets or sets full name.
/// </summary>
[Display(Name = "Subscriber full name")]
public string FullName { get; set; }
[Display(Name = "Name")]
public string NameFromOpenIdConnect { get; set; }
/// <summary>
/// Gets or sets new plan ID.
@ -89,13 +90,49 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets the purchaser email.
/// </summary>
[Display(Name = "Purchaser email")]
public string PurchaserEmail { get; set; }
[Display(Name = "Purchaser user principal name from marketplace API")]
public string PurchaserUPN { get; set; }
/// <summary>
/// Gets or sets the purchaser tenant ID.
/// </summary>
[Display(Name = "Purchaser AAD TenantId")]
public Guid PurchaserTenantId { get; set; }
/// <summary>
/// Gets or sets the beneficiary email.
/// </summary>
[Display(Name = "Beneficiary user principal name from marketplace API")]
public string BeneficiaryUPN { get; set; }
/// <summary>
/// Gets or sets the beneficiary tenant ID.
/// </summary>
[Display(Name = "Beneficiary AAD TenantId")]
public Guid BeneficiaryTenantId { get; set; }
/// <summary>
/// Gets or sets the email address received from the Graph API.
/// </summary>
[Display(Name = "Purchaser email")]
public string EmailFromGraph { get; set; }
/// <summary>
/// Gets or sets the purchaser name received from the Graph API.
/// </summary>
[Display(Name = "Purchaser name")]
public string NameFromGraph { get; set; }
/// <summary>
/// Gets or sets the user principal name received from the Graph API.
/// </summary>
[Display(Name = "User Principal Name")]
public string UserPrincipalName { get; set; }
/// <summary>
/// Gets or sets the purchaser email.
/// </summary>
[Display(Name = "Purchaser Email")]
public string PurchaserEmail { get; set; }
}
}

Просмотреть файл

@ -7,7 +7,7 @@ namespace CommandCenter.Models
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using CommandCenter.DimensionUsageStore;
using CommandCenter.Metering;
/// <summary>
/// Dimension view model.
@ -17,38 +17,45 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets the SubscriptionId.
/// </summary>
[Display(Name = "Subscription Id")]
public Guid SubscriptionId { get; set; }
/// <summary>
/// Gets or sets the SubscriptionName.
/// </summary>
[Display(Name = "Subscription Name")]
public string SubscriptionName { get; set; }
/// <summary>
/// Gets or sets the OfferId.
/// </summary>
[Display(Name = "Offer Id")]
public string OfferId { get; set; }
/// <summary>
/// Gets or sets the PlanId.
/// </summary>
[Display(Name = "Plan Id")]
public string PlanId { get; set; }
/// <summary>
/// Gets or sets the SubscriptionDimensions.
/// </summary>
[Display(Name = "Subscription Dimensions")]
public IEnumerable<string> SubscriptionDimensions { get; set; }
/// <summary>
/// Gets or sets the SelectedDimension.
/// </summary>
[Required]
[Display(Name = "Selected dimension")]
public string SelectedDimension { get; set; }
/// <summary>
/// Gets or sets the Quantity.
/// </summary>
[Required]
[Display(Name = "Quantity")]
public int Quantity { get; set; }
/// <summary>
@ -61,6 +68,7 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets the PastUsageEvents.
/// </summary>
[Display(Name = "Past usage events")]
public IEnumerable<DimensionUsageRecord> PastUsageEvents { get; set; }
}
}

Просмотреть файл

@ -3,6 +3,7 @@
namespace CommandCenter.Models
{
using System.ComponentModel.DataAnnotations;
using CommandCenter.Marketplace;
/// <summary>
@ -13,11 +14,13 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets operation type.
/// </summary>
[Display(Name = "Operation type")]
public string OperationType { get; set; }
/// <summary>
/// Gets or sets webhook payload.
/// </summary>
[Display(Name = "Payload")]
public WebhookPayload Payload { get; set; }
}
}

Просмотреть файл

@ -4,6 +4,7 @@
namespace CommandCenter.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Marketplace.SaaS.Models;
/// <summary>
@ -14,11 +15,13 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets operations.
/// </summary>
[Display(Name = "Operations")]
public List<Operation> Operations { get; set; }
/// <summary>
/// Gets or sets subscription name.
/// </summary>
[Display(Name = "Subscription name")]
public string SubscriptionName { get; set; }
}
}

Просмотреть файл

@ -42,12 +42,12 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets purchaser tenant ID.
/// </summary>
public Guid PurchaserTenantId { get; set; }
public Guid BeneficiaryTenantId { get; set; }
/// <summary>
/// Gets or sets purchaser email.
/// </summary>
public string PurchaserEmail { get; set; }
public string BeneficiaryEmail { get; set; }
/// <summary>
/// Gets a value indicating whether there are existing operations.
@ -84,8 +84,8 @@ namespace CommandCenter.Models
OfferId = marketplaceSubscription.OfferId,
State = marketplaceSubscription.SaasSubscriptionStatus ?? SubscriptionStatusEnum.NotStarted,
SubscriptionName = marketplaceSubscription.Name,
PurchaserEmail = marketplaceSubscription.Purchaser.EmailId,
PurchaserTenantId = marketplaceSubscription.Purchaser.TenantId ?? Guid.Empty,
BeneficiaryEmail = marketplaceSubscription.Beneficiary.EmailId,
BeneficiaryTenantId = marketplaceSubscription.Beneficiary.TenantId ?? Guid.Empty,
};
}
}

Просмотреть файл

@ -5,6 +5,7 @@ namespace CommandCenter.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Marketplace.SaaS.Models;
/// <summary>
@ -15,31 +16,37 @@ namespace CommandCenter.Models
/// <summary>
/// Gets or sets available plans.
/// </summary>
[Display(Name = "Available plans")]
public IList<Plan> AvailablePlans { get; set; }
/// <summary>
/// Gets or sets the current plan.
/// </summary>
[Display(Name = "Current plan")]
public string CurrentPlan { get; set; }
/// <summary>
/// Gets or sets new plan.
/// </summary>
[Display(Name = "New plan")]
public string NewPlan { get; set; }
/// <summary>
/// Gets or sets a value indicating whether there are pending operations..
/// </summary>
[Display(Name = "Pending operations")]
public bool PendingOperations { get; set; }
/// <summary>
/// Gets or sets subscription ID.
/// </summary>
[Display(Name = "Subscription Id")]
public Guid SubscriptionId { get; set; }
/// <summary>
/// Gets or sets subscription name.
/// </summary>
[Display(Name = "Subscription name")]
public string SubscriptionName { get; set; }
}
}

Просмотреть файл

@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter
{
/// <summary>
/// Enum for the active notificaiton handler.
/// </summary>
public enum NotificationHandlerEnum
{
/// <summary>
/// Sending emails.
/// </summary>
EmailNotifications,
/// <summary>
/// Send queue messages.
/// </summary>
AzureQueueNotifications,
}
}

Просмотреть файл

@ -49,7 +49,7 @@ namespace CommandCenter.OperationsStore
query,
(key, rowKey, timestamp, properties, etag) => new OperationRecord(key, rowKey),
token,
cancellationToken).ConfigureAwait(false);
cancellationToken);
result.AddRange(segment.Results.Select(r => r));
@ -68,13 +68,13 @@ namespace CommandCenter.OperationsStore
{
var table = this.tableClient.GetTableReference(TableName);
await this.InitTable(cancellationToken).ConfigureAwait(false);
await this.InitTable(cancellationToken);
var entity = new OperationRecord(subscriptionId.ToString(), operationId.ToString());
var tableOperation = TableOperation.InsertOrMerge(entity);
await table.ExecuteAsync(tableOperation, cancellationToken).ConfigureAwait(false);
await table.ExecuteAsync(tableOperation, cancellationToken);
}
private async Task InitTable(CancellationToken cancellationToken = default)
@ -83,7 +83,7 @@ namespace CommandCenter.OperationsStore
{
var table = this.tableClient.GetTableReference(TableName);
await table.CreateIfNotExistsAsync(cancellationToken).ConfigureAwait(false);
await table.CreateIfNotExistsAsync(cancellationToken);
this.tableInitialized = true;
}
}

Просмотреть файл

@ -35,15 +35,13 @@ namespace CommandCenter
/// <returns>int.</returns>
public static int Main(string[] args)
{
// Add storage account logging if desired.
//CloudStorageAccount storage = CloudStorageAccount.Parse("");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
// Remove the commeant below if using storage account to log.
// .WriteTo.AzureTableStorage(storage)
.CreateLogger();

Просмотреть файл

@ -3,13 +3,15 @@
namespace CommandCenter
{
using System.Threading.Tasks;
using Azure.Identity;
using CommandCenter.Authorization;
using CommandCenter.AzureQueues;
using CommandCenter.Mail;
using CommandCenter.DimensionUsageStore;
using CommandCenter.Marketplace;
using CommandCenter.Metering;
using CommandCenter.OperationsStore;
using CommandCenter.Webhook;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@ -23,12 +25,9 @@ namespace CommandCenter
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Microsoft.Marketplace.SaaS;
using System.Threading.Tasks;
using Serilog;
using Microsoft.Marketplace.Metering;
using CommandCenter.DimensionUsageStore;
using Azure.Identity;
using Microsoft.Marketplace.SaaS;
using Serilog;
/// <summary>
/// ASP.NET core startup class.
@ -103,14 +102,19 @@ namespace CommandCenter
JwtBearerDefaults.AuthenticationScheme,
options =>
{
// Need to override the ValidAudience, since the incoming token has the app ID as the aud claim.
// Need to override the ValidAudience, since the incoming token has the app ID as the aud claim.
// Library expects it to be api://<appId> format.
options.TokenValidationParameters.ValidAudience = this.configuration["WebHookTokenParameters:ClientId"];
options.TokenValidationParameters.ValidAudience = this.configuration["WebHookTokenParameters:ClientId"];
options.TokenValidationParameters.ValidIssuer = $"https://sts.windows.net/{this.configuration["WebHookTokenParameters:TenantId"]}/";
});
// Enable AAD sign on on the landing page.
services.AddMicrosoftIdentityWebAppAuthentication(this.configuration, "AzureAd");
// Enable AAD sign on on the landing page and Graph API calling capabilities
services
.AddMicrosoftIdentityWebAppAuthentication(this.configuration, "AzureAd") // Sign on with AAD
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" }) // Call Graph API
.AddMicrosoftGraph() // Use defaults with Graph V1
.AddInMemoryTokenCaches(); // Add token caching
services.Configure<OpenIdConnectOptions>(options =>
{
options.Events.OnSignedOutCallbackRedirect = (context) =>
@ -158,18 +162,7 @@ namespace CommandCenter
services.TryAddScoped<IWebhookHandler, ContosoWebhookHandler>();
services.TryAddScoped<IMarketplaceProcessor, MarketplaceProcessor>();
var notificationHandler = this.configuration.GetSection("CommandCenter").Get<CommandCenterOptions>().ActiveNotificationHandler;
if (notificationHandler == NotificationHandlerEnum.EmailNotifications)
{
// It is email in this sample, but you can plug in anything that implements the interface and communicate with an existing API.
// In the email case, the existing API is the SendGrid API...
services.TryAddScoped<IMarketplaceNotificationHandler, CommandCenterEMailHelper>();
}
else
{
services.TryAddScoped<IMarketplaceNotificationHandler, AzureQueueNotificationHandler>();
}
services.TryAddScoped<IMarketplaceNotificationHandler, AzureQueueNotificationHandler>();
services.AddAuthorization(
options => options.AddPolicy(
@ -186,10 +179,11 @@ namespace CommandCenter
services.AddSingleton<IAuthorizationHandler, CommandCenterAdminHandler>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddRazorPages()
.AddMicrosoftIdentityUI();
services
.AddControllersWithViews()
.AddMicrosoftIdentityUI();
}
}
}

Просмотреть файл

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter
namespace CommandCenter.Utilities
{
using System.Net.Mail;

Просмотреть файл

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace CommandCenter
namespace CommandCenter.Utilities
{
using System;
using System.IO;
@ -27,8 +27,8 @@ namespace CommandCenter
public WebhookRequestLogger(RequestDelegate next, ILoggerFactory loggerFactory)
{
this.next = next;
this.logger = loggerFactory.CreateLogger<WebhookRequestLogger>();
this.memoryStreamManager = new RecyclableMemoryStreamManager();
logger = loggerFactory.CreateLogger<WebhookRequestLogger>();
memoryStreamManager = new RecyclableMemoryStreamManager();
}
/// <summary>
@ -47,12 +47,12 @@ namespace CommandCenter
{
context.Request.EnableBuffering();
await using var requestStreamCopy = this.memoryStreamManager.GetStream();
await context.Request.Body.CopyToAsync(requestStreamCopy).ConfigureAwait(false);
await using var requestStreamCopy = memoryStreamManager.GetStream();
await context.Request.Body.CopyToAsync(requestStreamCopy);
context.Request.Body.Position = 0;
context.Request.Body.Seek(0, SeekOrigin.Begin);
this.logger.LogInformation($"Webhook raw request {ReadStream(requestStreamCopy)}");
logger.LogInformation($"Webhook raw request {ReadStream(requestStreamCopy)}");
}
}

Просмотреть файл

@ -5,165 +5,293 @@
@{
ViewData["Title"] = "Index";
}
<p class="h2 mb-4 text-center">Let's configure your subscription...</p>
<h1>Configure subscription</h1>
@if (Model == default)
{
<span class="text-danger">@ViewBag.message, please check logs.</span>
<span class="text-danger">@ViewBag.message, please check logs.</span>
}
else
{
<p>The publisher can design a form like below to capture information from the subscriber to kick off the onboarding process for the new customer.</p>
<hr />
<hr />
<span class="text-danger">@Html.ValidationSummary(false)</span>
<span class="text-danger">@Html.ValidationSummary(false)</span>
<form asp-controller="LandingPage" asp-action="Index">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="accordion" id="landingPageComponents">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Recommended minimum for landing page. This is the only place a customer's contact information can be collected.
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne">
<div class="accordion-body">
<div class="panel panel-default">
<div class="row">
<div class="col-6">
<div class="card">
<div class="card-title">
<p class="fw-bold">Information from claims</p>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.EmailFromClaims):</label>
<form asp-controller="LandingPage" asp-action="Index">
<div class="container">
<dl class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.Email)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.Email)
<input asp-for="Email" type="hidden" class="form-control" />
</div>
</div>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.EmailFromClaims) </label>
<input asp-for="EmailFromClaims" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.NameFromOpenIdConnect):</label>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.FullName)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.FullName)
<input asp-for="FullName" type="hidden" class="form-control" />
</div>
</div>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.NameFromOpenIdConnect) </label>
<input asp-for="NameFromOpenIdConnect" type="hidden" class="form-control" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="card">
<div class="card-title">
<div class="panel panel-default">
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.OfferId)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.OfferId)
<input asp-for="OfferId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.PlanId)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.PlanId)
<input asp-for="PlanId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.PurchaserEmail)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.PurchaserEmail)
<input asp-for="PurchaserEmail" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.PurchaserTenantId)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.PurchaserTenantId)
<input asp-for="PurchaserTenantId" type="hidden" class="form-control" />
</div>
</div>
@if (Model.SubscriptionStatus == SubscriptionStatusEnum.Subscribed)
{
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.AvailablePlans)
</div>
<div class="col-sm-9">
<select asp-for="NewPlanId" asp-items="@(new SelectList(Model.AvailablePlans, "PlanId", "DisplayName"))" class="form-control">
<option value="">Choose a new plan, or plan you wish to be on.</option>
</select>
</div>
</div>
}
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.SubscriptionId)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.SubscriptionId)
<input asp-for="SubscriptionId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.SubscriptionName)
</div>
<div class="col-sm-9">
@Html.DisplayFor(model => model.SubscriptionName)
<input asp-for="SubscriptionName" type="hidden" class="form-control" />
</div>
</div>
</div>
<div class="panel panel-default">
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.Region)
</div>
<div class="col-sm-3">
<select asp-for="Region" asp-items="Html.GetEnumSelectList<TargetContosoRegionEnum>()" class="form-control">
<option selected="selected" value="">Please select</option>
</select>
</div>
<div class="col-sm-9"></div>
</div>
<div class="row">
<div class="col-sm-3">
@Html.DisplayNameFor(model => model.BusinessUnitContactEmail)
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-sm-3">
<input asp-for="BusinessUnitContactEmail" type="email" class="form-control" />
<p class="fw-bold">Information from Graph API</p>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.EmailFromGraph):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.EmailFromGraph) </label>
<input asp-for="EmailFromGraph" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.NameFromGraph):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.NameFromGraph) </label>
<input asp-for="NameFromGraph" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.UserPrincipalName):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.UserPrincipalName) </label>
<input asp-for="UserPrincipalName" type="hidden" class="form-control" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-9">
<p>*The purchaser (who sees this page) can be different than the actual user of the solution.</p>
</div>
<br />
<div class="row">
<div class="col-6">
<div class="card">
<div class="card-title">
<p class="fw-bold">Information from Fulfillment API</p>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.PurchaserUPN):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.PurchaserUPN) </label>
<input asp-for="PurchaserUPN" type="hidden" class="form-control" />
</div>
</div>
<br />
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.BeneficiaryUPN):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.BeneficiaryUPN) </label>
<input asp-for="BeneficiaryUPN" type="hidden" class="form-control" />
</div>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="card">
<div class="card-title">
<p class="fw-bold">Customer contact email</p>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.PurchaserEmail):</label>
<input asp-for="PurchaserEmail" type="email" class="form-control" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
You can collect additional information as the first step of the onboarding process.
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse show" aria-labelledby="headingTwo">
<div class="accordion-body">
<div class="panel panel-default">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.Region):</label>
</div>
<div class="col-sm-6">
@Html.DropDownListFor(m => m.Region, new SelectList(Enum.GetValues(typeof(TargetContosoRegionEnum))), "Select region")
</div>
</div>
<br />
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.BusinessUnitContactEmail):</label>
</div>
<div class="col-sm-6">
<div class="row">
<div class="col-sm-6">
<input asp-for="BusinessUnitContactEmail" type="email" class="form-control" />
</div>
<div class="col-sm-6">
<p>*The purchaser (who sees this page) can be different than the actual user of the solution.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="true" aria-controls="collapseThree">
Example items from the marketplace API result.
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse show" aria-labelledby="headingThree">
<div class="accordion-body">
<div class="panel panel-default">
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.OfferId):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.OfferId) </label>
<input asp-for="OfferId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.PlanId):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.PlanId) </label>
<input asp-for="PlanId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.PurchaserTenantId):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.PurchaserTenantId) </label>
<input asp-for="PurchaserTenantId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.BeneficiaryTenantId):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.BeneficiaryTenantId) </label>
<input asp-for="BeneficiaryTenantId" type="hidden" class="form-control" />
</div>
</div>
@if (Model.SubscriptionStatus == SubscriptionStatusEnum.Subscribed)
{
<div class="row">
<div class="col-sm-6">
@Html.DisplayNameFor(model => model.AvailablePlans)
</div>
<div class="col-sm-6">
@Html.DropDownListFor(m => m.Region, new SelectList(Model.AvailablePlans, "PlanId", "DisplayName"), "Choose a new plan")
</div>
</div>
}
<div class="row">
<div class="col-sm-6">
<label class="form-label">@Html.DisplayNameFor(model => model.SubscriptionId):</label>
</div>
<div class="col-sm-6">
<label class="form-label">@Html.DisplayFor(model => model.SubscriptionId) </label>
<input asp-for="SubscriptionId" type="hidden" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
@Html.DisplayNameFor(model => model.SubscriptionName)
</div>
<div class="col-sm-6">
@Html.DisplayFor(model => model.SubscriptionName)
<input asp-for="SubscriptionName" type="hidden" class="form-control" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</dl>
@* Just to carry over the subscription status, so we can determine if we are here for initial
provisioning or configuration later in the life of the subscription*@
<input asp-for="SubscriptionStatus" type="hidden" class="form-control" />
</div>
</div>
<div>
<input type="submit"
value="Submit"
class="btn btn-primary" />
</div>
</form>
}
</div>
@* Just to carry over the subscription status, so we can determine if we are here for initial
provisioning or configuration later in the life of the subscription*@
<input asp-for="SubscriptionStatus" type="hidden" class="form-control" />
<button type="submit" class="btn btn-primary btn-block m-3">Submit</button>
</form>
}
@section Scripts
{
<script>
</script>
}

Просмотреть файл

@ -9,11 +9,12 @@
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css"/>
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/>
asp-fallback-test-class="visually-hidden" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
asp-suppress-fallback-integrity="true"
crossorigin="anonymous"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC">
</environment>
<link rel="stylesheet" href="~/css/site.css"/>
</head>
@ -42,22 +43,26 @@
</div>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
asp-fallback-src="~/lib/jquery/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
asp-suppress-fallback-integrity="true"
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
asp-suppress-fallback-integrity="true"
crossorigin="anonymous">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

Просмотреть файл

@ -5,19 +5,13 @@
<span class="navbar-text text-dark" style="margin-right: 50px">Hello @User.Identity.Name!</span>
</li>
<li class="nav-item">
<button type="button" class="btn btn-warning" style="border-bottom-width: 0px; border-top-width: 0px; padding-bottom: 0px; padding-top: 0px;">
<a class="nav-link text-dark btn btn-warning" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
</button>
<a class="btn btn-primary" role="button" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark btn" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
<a class="nav-link btn btn-primary" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
</li>
}
</ul>

Просмотреть файл

@ -2,132 +2,157 @@
@using CommandCenter.Models
@model IEnumerable<CommandCenter.Models.SubscriptionViewModel>
@{
ViewData["Title"] = "All Offer Subscriptions";
ViewData["Title"] = "Subscriptions";
}
<div class="container-fluid">
<h1>Subscriptions for all of the offers</h1>
<br />
<div class="container">
<div class="row">
<h1 class="mb-3">Subscriptions</h1>
<span class="text-danger">@Html.ValidationSummary(false)</span>
<span class="anchor">Status filter: </span>
<div class="d-inline-block">
@foreach (var status in Enum.GetNames(typeof(SubscriptionStatusEnum)))
{
</div>
<input type="checkbox" class="statusCheckbox" id="@status" checked/> @status
}
<div class="row">
<div class="col-12">
<form class="mb-4">
<fieldset class="border" id="stateFiltersFieldSet">
<legend style="float:none; width: auto !important;" class="m-2 px-2 fs-5" id="stateFiltersFieldSetLegend">State filter</legend>
@foreach (var status in Enum.GetNames(typeof(SubscriptionStatusEnum)))
{
<div class="form-check form-switch form-check-inline m-3">
<input class="form-check-input statusCheckbox" type="checkbox" id="@status" checked>
<label class="form-check-label" for="@status">@status</label>
</div>
}
</fieldset>
</form>
</div>
<table class="table table-sm w-auto text-xsmall">
<thead class="thead-dark">
<tr>
<th>
@Html.DisplayNameFor(model => model.SubscriptionName)
</th>
<th>
@Html.DisplayNameFor(model => model.State)
</th>
<th>
@Html.DisplayNameFor(model => model.OfferId)
</th>
<th>
@Html.DisplayNameFor(model => model.PlanId)
</th>
<th>
@Html.DisplayNameFor(model => model.PurchaserEmail)
</th>
</div>
<th>
@Html.DisplayNameFor(model => model.PurchaserTenantId)
</th>
<div class="row">
<div class="col-12">
<div class="table-responsive-sm">
<table class="table table-sm w-auto text-xsmall" id="subscriptions">
<thead class="thead-dark">
<tr>
<th>
@Html.DisplayNameFor(model => model.SubscriptionName)
</th>
<th>
@Html.DisplayNameFor(model => model.State)
</th>
<th>
@Html.DisplayNameFor(model => model.OfferId)
</th>
<th>
@Html.DisplayNameFor(model => model.PlanId)
</th>
<th>
@Html.DisplayNameFor(model => model.BeneficiaryEmail)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.BeneficiaryTenantId)
</th>
<th>
@Html.DisplayNameFor(model => model.SubscriptionId)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>Actions</th>
<th>Pending operations</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr class="@item.State">
<td>
@Html.DisplayFor(modelItem => item.SubscriptionName)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.OfferId)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlanId)
</td>
<td>
@Html.DisplayFor(modelItem => item.PurchaserEmail)
</td>
<th>
@Html.DisplayNameFor(model => model.SubscriptionId)
</th>
<td>
@Html.DisplayFor(modelItem => item.PurchaserTenantId)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.SubscriptionId)
</td>
<td>
<ul class="list-inline">
@if (!item.PendingOperations)
{
@foreach (var a in item.NextActions)
{
<li>
<span> @Html.ActionLink(Enum.GetName(typeof(ActionsEnum), a), "SubscriptionAction", new { subscriptionAction = a, subscriptionId = item.SubscriptionId }, new {@class = "list-inline-item" }) </span>
</li>
}
}
else
{
<li>Pending operations</li>
}
@if (item.IsDimensionEnabled && item.State != SubscriptionStatusEnum.PendingFulfillmentStart)
{
<li> @Html.ActionLink("SendDimensionUsage", "SubscriptionDimensionUsage", new { item.SubscriptionId }) </li>
}
</ul>
</td>
<td>
@if (item.ExistingOperations)
<th>Actions</th>
<th>Pending operations</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
@Html.ActionLink("Operations", "Operations", new { item.SubscriptionId })
<tr class="@item.State">
<td>
@Html.DisplayFor(modelItem => item.SubscriptionName)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.OfferId)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlanId)
</td>
<td>
@Html.DisplayFor(modelItem => item.BeneficiaryEmail)
</td>
<td>
@Html.DisplayFor(modelItem => item.BeneficiaryTenantId)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.SubscriptionId)
</td>
<td>
<ul class="list-inline">
@if (!item.PendingOperations)
{
@foreach (var a in item.NextActions)
{
<li>
<span> @Html.ActionLink(Enum.GetName(typeof(ActionsEnum), a), "SubscriptionAction", new { subscriptionAction = a, subscriptionId = item.SubscriptionId }, new { @class = "list-inline-item" }) </span>
</li>
}
}
else
{
<li>Pending operations</li>
}
@if (item.IsDimensionEnabled && item.State != SubscriptionStatusEnum.PendingFulfillmentStart)
{
<li> @Html.ActionLink("SendDimensionUsage", "SubscriptionDimensionUsage", new { item.SubscriptionId }) </li>
}
</ul>
</td>
<td>
@if (item.ExistingOperations)
{
@Html.ActionLink("Operations", "Operations", new { item.SubscriptionId })
}
</td>
</tr>
}
</td>
</tr>
}
</tbody>
</table>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script>
$(document).ready(function () {
$(".statusCheckbox").change(function () {
if (this.checked) {
$("tr." + this.id).show();
}
else {
$("tr." + this.id).hide();
}
@section Scripts
{
<script>
$(document).ready(function () {
$(".statusCheckbox").change(function () {
if (this.checked) {
$("tr." + this.id).show();
}
else {
$("tr." + this.id).hide();
}
});
$("#subscriptions tr.Subscribed").addClass("table-success");
$("#subscriptions tr.PendingFulfillmentStart").addClass("table-info");
$("#subscriptions tr.Unsubscribed").addClass("table-secondary");
});
});
</script>
</script>
}

Просмотреть файл

@ -65,7 +65,7 @@
</div>
</div>
<div class="form-group">
<input type="submit" value="Send Usage" class="btn btn-primary" />
<button type="submit" class="btn btn-primary btn-block m-3">Send Usage</button>
</div>
</form>
</div>

Просмотреть файл

@ -1,6 +1,6 @@
@model CommandCenter.Models.UpdateSubscriptionViewModel
@{
ViewData["Title"] = "All Offer Subscriptions";
ViewData["Title"] = "All Offer Subscription";
}
<h1>Update Subscriptions</h1>
@ -11,41 +11,37 @@
<form asp-action="UpdateSubscription">
<div>
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.SubscriptionName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.SubscriptionName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.SubscriptionId)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.SubscriptionId)
<div class="row">
<div class="col-sm-2">
@Html.DisplayNameFor(m => m.SubscriptionName)
</div>
<div class="col-sm-10">
@Html.DisplayFor(m => m.SubscriptionName)
</div>
<div class="col-sm-2">
@Html.DisplayNameFor(m => m.SubscriptionId)
</div>
<div class="col-sm-10">
@Html.DisplayFor(m => m.SubscriptionId)
<input asp-for="SubscriptionId" type="hidden" class="form-control" />
</dd>
</div>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.CurrentPlan)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.CurrentPlan)
</dd>
<div class="col-sm-2">
@Html.DisplayNameFor(m => m.CurrentPlan)
</div>
<div class="col-sm-10">
@Html.DisplayFor(m => m.CurrentPlan)
</div>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.AvailablePlans)
</dt>
<dd class="col-sm-10">
<select asp-for="NewPlan" asp-items="@(new SelectList(Model.AvailablePlans, "PlanId", "DisplayName"))" class="form-control">
<option value="">Choose a new plan</option>
</select>
</dd>
</dl>
<div class="col-sm-2">
@Html.DisplayNameFor(m => m.AvailablePlans)
</div>
<div class="col-sm-10">
@Html.DropDownListFor(m => m.AvailablePlans, new SelectList(Model.AvailablePlans, "PlanId", "DisplayName"), "Choose a new plan")
</div>
</div>
</div>
<div>
<input type="submit"
value="Submit"
class="btn btn-primary" />
<button type="submit" class="btn btn-primary btn-block m-3">Submit</button>
</div>
</form>

Просмотреть файл

@ -4,6 +4,7 @@
namespace CommandCenter.Webhook
{
using System;
using System.Threading;
using System.Threading.Tasks;
using CommandCenter.Marketplace;
using Microsoft.Marketplace.SaaS;
@ -33,84 +34,49 @@ namespace CommandCenter.Webhook
/// <inheritdoc/>
public async Task ChangePlanAsync(WebhookPayload payload)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
if (payload.Status == OperationStatusEnum.Succeeded)
{
await this.marketplaceClient.Operations.UpdateOperationStatusAsync(
payload.SubscriptionId,
payload.OperationId,
new UpdateOperation { PlanId = payload.PlanId, Status = UpdateOperationStatusEnum.Success }).ConfigureAwait(false);
}
else if (payload.Status == OperationStatusEnum.Conflict || payload.Status == OperationStatusEnum.Failed)
{
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload).ConfigureAwait(false);
}
await this.NotifyAndAck(
payload,
new UpdateOperation { PlanId = payload.PlanId, Status = UpdateOperationStatusEnum.Success },
this.notificationHelper.NotifyChangePlanAsync);
}
/// <inheritdoc/>
public async Task ChangeQuantityAsync(WebhookPayload payload)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
if (payload.Status == OperationStatusEnum.Succeeded)
{
await this.marketplaceClient.Operations.UpdateOperationStatusAsync(
payload.SubscriptionId,
payload.OperationId,
new UpdateOperation { Quantity = payload.Quantity, Status = UpdateOperationStatusEnum.Success }).ConfigureAwait(false);
}
else if (payload.Status == OperationStatusEnum.Conflict || payload.Status == OperationStatusEnum.Failed)
{
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload).ConfigureAwait(false);
}
await this.NotifyAndAck(
payload,
new UpdateOperation { Quantity = payload.Quantity, Status = UpdateOperationStatusEnum.Success },
this.notificationHelper.NotifyChangeQuantityAsync);
}
/// <inheritdoc/>
public async Task ReinstatedAsync(WebhookPayload payload)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
if (payload.Status == OperationStatusEnum.Succeeded)
{
await this.notificationHelper.NotifyReinstatedAsync(payload).ConfigureAwait(false);
}
else if (payload.Status == OperationStatusEnum.Conflict || payload.Status == OperationStatusEnum.Failed)
{
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload).ConfigureAwait(false);
}
await this.NotifyAndAck(
payload,
new UpdateOperation { Status = UpdateOperationStatusEnum.Success },
this.notificationHelper.NotifyReinstatedAsync);
}
/// <inheritdoc/>
public async Task SuspendedAsync(WebhookPayload payload)
{
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
if (payload.Status == OperationStatusEnum.Succeeded)
{
await this.notificationHelper.NotifySuspendedAsync(payload)
.ConfigureAwait(false);
}
else if (payload.Status == OperationStatusEnum.Conflict || payload.Status == OperationStatusEnum.Failed)
{
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload).ConfigureAwait(false);
}
await this.NotifyAndAck(
payload,
new UpdateOperation { Status = UpdateOperationStatusEnum.Success },
this.notificationHelper.NotifySuspendedAsync);
}
/// <inheritdoc/>
public async Task UnsubscribedAsync(WebhookPayload payload)
{
await this.NotifyAndAck(
payload,
new UpdateOperation { Status = UpdateOperationStatusEnum.Success },
this.notificationHelper.NotifyUnsubscribedAsync);
}
private async Task NotifyAndAck(WebhookPayload payload, UpdateOperation updateOperation, Func<WebhookPayload, CancellationToken, Task> notify)
{
if (payload == null)
{
@ -119,11 +85,15 @@ namespace CommandCenter.Webhook
if (payload.Status == OperationStatusEnum.Succeeded)
{
await this.notificationHelper.NotifyUnsubscribedAsync(payload).ConfigureAwait(false);
await notify(payload, CancellationToken.None);
await this.marketplaceClient.Operations.UpdateOperationStatusAsync(
payload.SubscriptionId,
payload.OperationId,
updateOperation);
}
else if (payload.Status == OperationStatusEnum.Conflict || payload.Status == OperationStatusEnum.Failed)
{
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload).ConfigureAwait(false);
await this.notificationHelper.ProcessOperationFailOrConflictAsync(payload);
}
}
}

Просмотреть файл

@ -6,6 +6,8 @@
// Register a multi-teanant application, and do not change the "TenantId" from common auth endpoint *value should be organizations or common).
"TenantId": "common",
"ClientId": "CHANGE",
// Following is used for calling the Graph API
"ClientSecret": "CHANGE",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc"
},
@ -35,13 +37,12 @@
},
"CommandCenterAdmin": "CHANGE",
"ShowUnsubscribed": "false",
"ActiveNotificationHandler": "AzureQueueNotifications",
"AzureQueue": {
"StorageConnectionString": "CHANGE",
"QueueName": "notifications"
},
// Specify Id, PlanIds and OfferIds which participate in each dimension. Add more dimensions as needed
"EnableDimensionMeterReporting": "false",
"EnableDimensionMeterReporting": "true",
"Dimensions": [
{
"DimensionId": "",

Просмотреть файл

@ -0,0 +1,15 @@
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.6.0",
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "jsdelivr",
"library": "bootstrap@5.0.2",
"destination": "wwwroot/lib/bootstrap/"
}
]
}

Просмотреть файл

@ -54,11 +54,9 @@ body {
[class*="span"] [class*="span"] { background: #FEE; }
/*
tr.Subscribed { background: lightgreen; }
tr.PendingFulfillmentStart { background: lightsalmon; }
tr.PendingFulfillmentStart { background: lightsalmon; }
tr.Unsubscribed { background: lightgray; }
tr.Unsubscribed { background: lightgray; }*/

Просмотреть файл

@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2011-2018 Twitter, Inc.
Copyright (c) 2011-2018 The Bootstrap Authors
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

Просмотреть файл

@ -0,0 +1,239 @@
<p align="center">
<a href="https://getbootstrap.com/">
<img src="https://getbootstrap.com/docs/5.0/assets/brand/bootstrap-logo-shadow.png" alt="Bootstrap logo" width="200" height="165">
</a>
</p>
<h3 align="center">Bootstrap</h3>
<p align="center">
Sleek, intuitive, and powerful front-end framework for faster and easier web development.
<br>
<a href="https://getbootstrap.com/docs/5.0/"><strong>Explore Bootstrap docs »</strong></a>
<br>
<br>
<a href="https://github.com/twbs/bootstrap/issues/new?template=bug_report.md">Report bug</a>
·
<a href="https://github.com/twbs/bootstrap/issues/new?template=feature_request.md">Request feature</a>
·
<a href="https://themes.getbootstrap.com/">Themes</a>
·
<a href="https://blog.getbootstrap.com/">Blog</a>
</p>
## Bootstrap 5
Our default branch is for development of our Bootstrap 5 release. Head to the [`v4-dev` branch](https://github.com/twbs/bootstrap/tree/v4-dev) to view the readme, documentation, and source code for Bootstrap 4.
## Table of contents
- [Quick start](#quick-start)
- [Status](#status)
- [What's included](#whats-included)
- [Bugs and feature requests](#bugs-and-feature-requests)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [Community](#community)
- [Versioning](#versioning)
- [Creators](#creators)
- [Thanks](#thanks)
- [Copyright and license](#copyright-and-license)
## Quick start
Several quick start options are available:
- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.0.2.zip)
- Clone the repo: `git clone https://github.com/twbs/bootstrap.git`
- Install with [npm](https://www.npmjs.com/): `npm install bootstrap`
- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap`
- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.0.2`
- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass`
Read the [Getting started page](https://getbootstrap.com/docs/5.0/getting-started/introduction/) for information on the framework contents, templates and examples, and more.
## Status
[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/)
[![Build Status](https://img.shields.io/github/workflow/status/twbs/bootstrap/JS%20Tests/main?label=JS%20Tests&logo=github)](https://github.com/twbs/bootstrap/actions?query=workflow%3AJS+Tests+branch%3Amain)
[![npm version](https://img.shields.io/npm/v/bootstrap)](https://www.npmjs.com/package/bootstrap)
[![Gem version](https://img.shields.io/gem/v/bootstrap)](https://rubygems.org/gems/bootstrap)
[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap)
[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap)
[![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest)
[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer)
[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev)
[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main)](https://coveralls.io/github/twbs/bootstrap?branch=main)
[![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
[![CSS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=brotli&label=CSS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
[![JS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js)
[![JS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=brotli&label=JS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js)
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)
[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap)](#backers)
[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap)](#sponsors)
## What's included
Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this:
```text
bootstrap/
├── css/
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
│ ├── bootstrap.rtl.min.css
│ └── bootstrap.rtl.min.css.map
└── js/
├── bootstrap.bundle.js
├── bootstrap.bundle.js.map
├── bootstrap.bundle.min.js
├── bootstrap.bundle.min.js.map
├── bootstrap.esm.js
├── bootstrap.esm.js.map
├── bootstrap.esm.min.js
├── bootstrap.esm.min.js.map
├── bootstrap.js
├── bootstrap.js.map
├── bootstrap.min.js
└── bootstrap.min.js.map
```
We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/).
## Bugs and feature requests
Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new).
## Documentation
Bootstrap's documentation, included in this repo in the root directory, is built with [Hugo](https://gohugo.io/) and publicly hosted on GitHub Pages at <https://getbootstrap.com/>. The docs may also be run locally.
Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/assets/js/search.js`.
### Running documentation locally
1. Run `npm install` to install the Node.js dependencies, including Hugo (the site builder).
2. Run `npm run test` (or a specific npm script) to rebuild distributed CSS and JavaScript files, as well as our docs assets.
3. From the root `/bootstrap` directory, run `npm run docs-serve` in the command line.
4. Open `http://localhost:9001/` in your browser, and voilà.
Learn more about using Hugo by reading its [documentation](https://gohugo.io/documentation/).
### Documentation for previous releases
You can find all our previous releases docs on <https://getbootstrap.com/docs/versions/>.
[Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download.
## Contributing
Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development.
Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/main/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo).
Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/main/.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org/>.
## Community
Get updates on Bootstrap's development and chat with the project maintainers and community members.
- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap).
- Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/).
- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/).
- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel.
- Implementation help may be found at Stack Overflow (tagged [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5)).
- Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability.
## Versioning
For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible.
See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release.
## Creators
**Mark Otto**
- <https://twitter.com/mdo>
- <https://github.com/mdo>
**Jacob Thornton**
- <https://twitter.com/fat>
- <https://github.com/fat>
## Thanks
<a href="https://www.browserstack.com/">
<img src="https://live.browserstack.com/images/opensource/browserstack-logo.svg" alt="BrowserStack Logo" width="192" height="42">
</a>
Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers!
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)]
[![OC sponsor 0](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website)
[![OC sponsor 1](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website)
[![OC sponsor 2](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website)
[![OC sponsor 3](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website)
[![OC sponsor 4](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website)
[![OC sponsor 5](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website)
[![OC sponsor 6](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website)
[![OC sponsor 7](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website)
[![OC sponsor 8](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website)
[![OC sponsor 9](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website)
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)]
[![Backers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers)
## Copyright and license
Code and documentation copyright 2011–2021 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/).

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,178 +1,284 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after { box-sizing: border-box; }
html {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-text-size-adjust: 100%;
font-family: sans-serif;
line-height: 1.15;
*::after {
box-sizing: border-box;
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { display: block; }
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
background-color: #fff;
color: #212529;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
margin: 0;
text-align: left;
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
[tabindex="-1"]:focus { outline: 0 !important; }
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0.5rem;
margin-top: 0;
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-bottom: 1rem;
margin-top: 0;
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
-webkit-text-decoration: underline dotted;
-webkit-text-decoration-skip-ink: none;
border-bottom: 0;
cursor: help;
text-decoration: underline;
text-decoration: underline dotted;
text-decoration-skip-ink: none;
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
font-style: normal;
line-height: inherit;
margin-bottom: 1rem;
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-bottom: 1rem;
margin-top: 0;
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol { margin-bottom: 0; }
dt { font-weight: 700; }
dd {
margin-bottom: .5rem;
margin-left: 0;
ul ol {
margin-bottom: 0;
}
blockquote { margin: 0 0 1rem; }
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong { font-weight: bolder; }
strong {
font-weight: bolder;
}
small { font-size: 80%; }
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub { bottom: -.25em; }
sub {
bottom: -0.25em;
}
sup { top: -.5em; }
sup {
top: -0.5em;
}
a {
background-color: transparent;
color: #007bff;
text-decoration: none;
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0056b3;
text-decoration: underline;
color: #0a58ca;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus { outline: 0; }
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
margin-bottom: 1rem;
margin-top: 0;
overflow: auto;
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
figure { margin: 0 0 1rem; }
img {
border-style: none;
vertical-align: middle;
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
overflow: hidden;
vertical-align: middle;
vertical-align: middle;
}
table { border-collapse: collapse; }
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
caption-side: bottom;
color: #6c757d;
padding-bottom: 0.75rem;
padding-top: 0.75rem;
text-align: left;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th { text-align: inherit; }
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
display: inline-block;
}
button { border-radius: 0; }
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
button:focus:not(:focus-visible) {
outline: 0;
}
input,
@ -180,98 +286,141 @@ button,
select,
optgroup,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
margin: 0;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input { overflow: visible; }
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
select { text-transform: none; }
select { word-wrap: normal; }
button,
[type="button"],
[type="reset"],
[type="submit"] { -webkit-appearance: button; }
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) { cursor: pointer; }
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] { -webkit-appearance: listbox; }
textarea {
overflow: auto;
resize: vertical;
resize: vertical;
}
fieldset {
border: 0;
margin: 0;
min-width: 0;
padding: 0;
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
color: inherit;
display: block;
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
line-height: inherit;
margin-bottom: .5rem;
max-width: 100%;
padding: 0;
white-space: normal;
width: 100%;
}
}
legend + * {
clear: left;
}
progress { vertical-align: baseline; }
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button { height: auto; }
[type="search"] {
-webkit-appearance: none;
outline-offset: -2px;
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
font: inherit;
-webkit-appearance: button;
}
output { display: inline-block; }
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
cursor: pointer;
display: list-item;
display: list-item;
cursor: pointer;
}
template { display: none; }
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
[hidden] { display: none !important; }
/*# sourceMappingURL=bootstrap-reboot.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,8 +1,8 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,423 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

10813
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

4967
src/CommandCenter/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

205
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/alert.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,205 @@
/*!
* Bootstrap alert.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/selector-engine.js'), require('./dom/event-handler.js'), require('./base-component.js')) :
typeof define === 'function' && define.amd ? define(['./dom/selector-engine', './dom/event-handler', './base-component'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Alert = factory(global.SelectorEngine, global.EventHandler, global.Base));
}(this, (function (SelectorEngine, EventHandler, BaseComponent) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
var BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent);
const getSelector = element => {
let selector = element.getAttribute('data-bs-target');
if (!selector || selector === '#') {
let hrefAttr = element.getAttribute('href'); // The only valid content that could double as a selector are IDs or classes,
// so everything starting with `#` or `.`. If a "real" URL is used as the selector,
// `document.querySelector` will rightfully complain it is invalid.
// See https://github.com/twbs/bootstrap/issues/32273
if (!hrefAttr || !hrefAttr.includes('#') && !hrefAttr.startsWith('.')) {
return null;
} // Just in case some CMS puts out a full URL with the anchor appended
if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
hrefAttr = `#${hrefAttr.split('#')[1]}`;
}
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null;
}
return selector;
};
const getElementFromSelector = element => {
const selector = getSelector(element);
return selector ? document.querySelector(selector) : null;
};
const getjQuery = () => {
const {
jQuery
} = window;
if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return jQuery;
}
return null;
};
const DOMContentLoadedCallbacks = [];
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
DOMContentLoadedCallbacks.forEach(callback => callback());
});
}
DOMContentLoadedCallbacks.push(callback);
} else {
callback();
}
};
const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
const $ = getjQuery();
/* istanbul ignore if */
if ($) {
const name = plugin.NAME;
const JQUERY_NO_CONFLICT = $.fn[name];
$.fn[name] = plugin.jQueryInterface;
$.fn[name].Constructor = plugin;
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT;
return plugin.jQueryInterface;
};
}
});
};
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.2): alert.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'alert';
const DATA_KEY = 'bs.alert';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const SELECTOR_DISMISS = '[data-bs-dismiss="alert"]';
const EVENT_CLOSE = `close${EVENT_KEY}`;
const EVENT_CLOSED = `closed${EVENT_KEY}`;
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
const CLASS_NAME_ALERT = 'alert';
const CLASS_NAME_FADE = 'fade';
const CLASS_NAME_SHOW = 'show';
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class Alert extends BaseComponent__default['default'] {
// Getters
static get NAME() {
return NAME;
} // Public
close(element) {
const rootElement = element ? this._getRootElement(element) : this._element;
const customEvent = this._triggerCloseEvent(rootElement);
if (customEvent === null || customEvent.defaultPrevented) {
return;
}
this._removeElement(rootElement);
} // Private
_getRootElement(element) {
return getElementFromSelector(element) || element.closest(`.${CLASS_NAME_ALERT}`);
}
_triggerCloseEvent(element) {
return EventHandler__default['default'].trigger(element, EVENT_CLOSE);
}
_removeElement(element) {
element.classList.remove(CLASS_NAME_SHOW);
const isAnimated = element.classList.contains(CLASS_NAME_FADE);
this._queueCallback(() => this._destroyElement(element), element, isAnimated);
}
_destroyElement(element) {
element.remove();
EventHandler__default['default'].trigger(element, EVENT_CLOSED);
} // Static
static jQueryInterface(config) {
return this.each(function () {
const data = Alert.getOrCreateInstance(this);
if (config === 'close') {
data[config](this);
}
});
}
static handleDismiss(alertInstance) {
return function (event) {
if (event) {
event.preventDefault();
}
alertInstance.close(this);
};
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
EventHandler__default['default'].on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDismiss(new Alert()));
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Alert to jQuery only if jQuery is present
*/
defineJQueryPlugin(Alert);
return Alert;
})));
//# sourceMappingURL=alert.js.map

1
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/alert.js.map поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

13
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/alert.min.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
/**
* Minified by jsDelivr using Terser v5.3.5.
* Original file: /npm/bootstrap@5.0.2/js/dist/alert.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Bootstrap alert.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("./dom/selector-engine.js"),require("./dom/event-handler.js"),require("./base-component.js")):"function"==typeof define&&define.amd?define(["./dom/selector-engine","./dom/event-handler","./base-component"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Alert=t(e.SelectorEngine,e.EventHandler,e.Base)}(this,(function(e,t,n){"use strict";function r(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s=r(t),o=r(n);const l=[];class i extends o.default{static get NAME(){return"alert"}close(e){const t=e?this._getRootElement(e):this._element,n=this._triggerCloseEvent(t);null===n||n.defaultPrevented||this._removeElement(t)}_getRootElement(e){return(e=>{const t=(e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n="#"+n.split("#")[1]),t=n&&"#"!==n?n.trim():null}return t})(e);return t?document.querySelector(t):null})(e)||e.closest(".alert")}_triggerCloseEvent(e){return s.default.trigger(e,"close.bs.alert")}_removeElement(e){e.classList.remove("show");const t=e.classList.contains("fade");this._queueCallback((()=>this._destroyElement(e)),e,t)}_destroyElement(e){e.remove(),s.default.trigger(e,"closed.bs.alert")}static jQueryInterface(e){return this.each((function(){const t=i.getOrCreateInstance(this);"close"===e&&t[e](this)}))}static handleDismiss(e){return function(t){t&&t.preventDefault(),e.close(this)}}}var a,u;return s.default.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',i.handleDismiss(new i)),a=i,u=()=>{const e=(()=>{const{jQuery:e}=window;return e&&!document.body.hasAttribute("data-bs-no-jquery")?e:null})();if(e){const t=a.NAME,n=e.fn[t];e.fn[t]=a.jQueryInterface,e.fn[t].Constructor=a,e.fn[t].noConflict=()=>(e.fn[t]=n,a.jQueryInterface)}},"loading"===document.readyState?(l.length||document.addEventListener("DOMContentLoaded",(()=>{l.forEach((e=>e()))})),l.push(u)):u(),i}));
//# sourceMappingURL=/sm/931feb2c96a88d59553742f47354717ac8657a4e53111bea25df4831479516f6.map

Просмотреть файл

@ -0,0 +1,178 @@
/*!
* Bootstrap base-component.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/data.js'), require('./dom/selector-engine.js'), require('./dom/event-handler.js')) :
typeof define === 'function' && define.amd ? define(['./dom/data', './dom/selector-engine', './dom/event-handler'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Base = factory(global.Data, global.SelectorEngine, global.EventHandler));
}(this, (function (Data, SelectorEngine, EventHandler) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var Data__default = /*#__PURE__*/_interopDefaultLegacy(Data);
var SelectorEngine__default = /*#__PURE__*/_interopDefaultLegacy(SelectorEngine);
var EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
const MILLISECONDS_MULTIPLIER = 1000;
const TRANSITION_END = 'transitionend'; // Shoutout AngusCroll (https://goo.gl/pxwQGp)
const getTransitionDurationFromElement = element => {
if (!element) {
return 0;
} // Get transition-duration of the element
let {
transitionDuration,
transitionDelay
} = window.getComputedStyle(element);
const floatTransitionDuration = Number.parseFloat(transitionDuration);
const floatTransitionDelay = Number.parseFloat(transitionDelay); // Return 0 if element or transition duration is not found
if (!floatTransitionDuration && !floatTransitionDelay) {
return 0;
} // If multiple durations are defined, take the first
transitionDuration = transitionDuration.split(',')[0];
transitionDelay = transitionDelay.split(',')[0];
return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;
};
const triggerTransitionEnd = element => {
element.dispatchEvent(new Event(TRANSITION_END));
};
const isElement = obj => {
if (!obj || typeof obj !== 'object') {
return false;
}
if (typeof obj.jquery !== 'undefined') {
obj = obj[0];
}
return typeof obj.nodeType !== 'undefined';
};
const getElement = obj => {
if (isElement(obj)) {
// it's a jQuery object or a node element
return obj.jquery ? obj[0] : obj;
}
if (typeof obj === 'string' && obj.length > 0) {
return SelectorEngine__default['default'].findOne(obj);
}
return null;
};
const execute = callback => {
if (typeof callback === 'function') {
callback();
}
};
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
if (!waitForTransition) {
execute(callback);
return;
}
const durationPadding = 5;
const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;
let called = false;
const handler = ({
target
}) => {
if (target !== transitionElement) {
return;
}
called = true;
transitionElement.removeEventListener(TRANSITION_END, handler);
execute(callback);
};
transitionElement.addEventListener(TRANSITION_END, handler);
setTimeout(() => {
if (!called) {
triggerTransitionEnd(transitionElement);
}
}, emulatedDuration);
};
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.2): base-component.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const VERSION = '5.0.2';
class BaseComponent {
constructor(element) {
element = getElement(element);
if (!element) {
return;
}
this._element = element;
Data__default['default'].set(this._element, this.constructor.DATA_KEY, this);
}
dispose() {
Data__default['default'].remove(this._element, this.constructor.DATA_KEY);
EventHandler__default['default'].off(this._element, this.constructor.EVENT_KEY);
Object.getOwnPropertyNames(this).forEach(propertyName => {
this[propertyName] = null;
});
}
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated);
}
/** Static */
static getInstance(element) {
return Data__default['default'].get(element, this.DATA_KEY);
}
static getOrCreateInstance(element, config = {}) {
return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);
}
static get VERSION() {
return VERSION;
}
static get NAME() {
throw new Error('You have to implement the static method "NAME", for each component!');
}
static get DATA_KEY() {
return `bs.${this.NAME}`;
}
static get EVENT_KEY() {
return `.${this.DATA_KEY}`;
}
}
return BaseComponent;
})));
//# sourceMappingURL=base-component.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,13 @@
/**
* Minified by jsDelivr using Terser v5.3.5.
* Original file: /npm/bootstrap@5.0.2/js/dist/base-component.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Bootstrap base-component.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("./dom/data.js"),require("./dom/selector-engine.js"),require("./dom/event-handler.js")):"function"==typeof define&&define.amd?define(["./dom/data","./dom/selector-engine","./dom/event-handler"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Base=t(e.Data,e.SelectorEngine,e.EventHandler)}(this,(function(e,t,n){"use strict";function r(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=r(e),s=r(t),i=r(n);const a="transitionend",u=e=>{"function"==typeof e&&e()},l=(e,t,n=!0)=>{if(!n)return void u(e);const r=(e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const r=Number.parseFloat(t),o=Number.parseFloat(n);return r||o?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0})(t)+5;let o=!1;const s=({target:n})=>{n===t&&(o=!0,t.removeEventListener(a,s),u(e))};t.addEventListener(a,s),setTimeout((()=>{o||t.dispatchEvent(new Event(a))}),r)};return class{constructor(e){var t;(e=(e=>!(!e||"object"!=typeof e)&&(void 0!==e.jquery&&(e=e[0]),void 0!==e.nodeType))(t=e)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?s.default.findOne(t):null)&&(this._element=e,o.default.set(this._element,this.constructor.DATA_KEY,this))}dispose(){o.default.remove(this._element,this.constructor.DATA_KEY),i.default.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((e=>{this[e]=null}))}_queueCallback(e,t,n=!0){l(e,t,n)}static getInstance(e){return o.default.get(e,this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return"5.0.2"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}}));
//# sourceMappingURL=/sm/9f0ccbd9f1a22d3c55c2ef5b92961a47bde4cef709ff9983295cf639f2d21bfa.map

139
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/button.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,139 @@
/*!
* Bootstrap button.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/selector-engine.js'), require('./dom/event-handler.js'), require('./base-component.js')) :
typeof define === 'function' && define.amd ? define(['./dom/selector-engine', './dom/event-handler', './base-component'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Button = factory(global.SelectorEngine, global.EventHandler, global.Base));
}(this, (function (SelectorEngine, EventHandler, BaseComponent) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
var BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent);
const getjQuery = () => {
const {
jQuery
} = window;
if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return jQuery;
}
return null;
};
const DOMContentLoadedCallbacks = [];
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
DOMContentLoadedCallbacks.forEach(callback => callback());
});
}
DOMContentLoadedCallbacks.push(callback);
} else {
callback();
}
};
const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
const $ = getjQuery();
/* istanbul ignore if */
if ($) {
const name = plugin.NAME;
const JQUERY_NO_CONFLICT = $.fn[name];
$.fn[name] = plugin.jQueryInterface;
$.fn[name].Constructor = plugin;
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT;
return plugin.jQueryInterface;
};
}
});
};
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.2): button.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'button';
const DATA_KEY = 'bs.button';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const CLASS_NAME_ACTIVE = 'active';
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]';
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class Button extends BaseComponent__default['default'] {
// Getters
static get NAME() {
return NAME;
} // Public
toggle() {
// Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method
this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE));
} // Static
static jQueryInterface(config) {
return this.each(function () {
const data = Button.getOrCreateInstance(this);
if (config === 'toggle') {
data[config]();
}
});
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
EventHandler__default['default'].on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
event.preventDefault();
const button = event.target.closest(SELECTOR_DATA_TOGGLE);
const data = Button.getOrCreateInstance(button);
data.toggle();
});
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Button to jQuery only if jQuery is present
*/
defineJQueryPlugin(Button);
return Button;
})));
//# sourceMappingURL=button.js.map

1
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/button.js.map поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

13
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/button.min.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
/**
* Minified by jsDelivr using Terser v5.3.5.
* Original file: /npm/bootstrap@5.0.2/js/dist/button.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Bootstrap button.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("./dom/selector-engine.js"),require("./dom/event-handler.js"),require("./base-component.js")):"function"==typeof define&&define.amd?define(["./dom/selector-engine","./dom/event-handler","./base-component"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Button=t(e.SelectorEngine,e.EventHandler,e.Base)}(this,(function(e,t,n){"use strict";function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var r=o(t),s=o(n);const a=[],u='[data-bs-toggle="button"]';class c extends s.default{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(e){return this.each((function(){const t=c.getOrCreateInstance(this);"toggle"===e&&t[e]()}))}}var i,d;return r.default.on(document,"click.bs.button.data-api",u,(e=>{e.preventDefault();const t=e.target.closest(u);c.getOrCreateInstance(t).toggle()})),i=c,d=()=>{const e=(()=>{const{jQuery:e}=window;return e&&!document.body.hasAttribute("data-bs-no-jquery")?e:null})();if(e){const t=i.NAME,n=e.fn[t];e.fn[t]=i.jQueryInterface,e.fn[t].Constructor=i,e.fn[t].noConflict=()=>(e.fn[t]=n,i.jQueryInterface)}},"loading"===document.readyState?(a.length||document.addEventListener("DOMContentLoaded",(()=>{a.forEach((e=>e()))})),a.push(d)):d(),c}));
//# sourceMappingURL=/sm/0a25e38cd12c76ce6dd62ccf97f5c90fb6cb5535ff785f26b193f158d437f5c7.map

721
src/CommandCenter/wwwroot/lib/bootstrap/js/dist/carousel.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,721 @@
/*!
* Bootstrap carousel.js v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/selector-engine.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./base-component.js')) :
typeof define === 'function' && define.amd ? define(['./dom/selector-engine', './dom/event-handler', './dom/manipulator', './base-component'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Carousel = factory(global.SelectorEngine, global.EventHandler, global.Manipulator, global.Base));
}(this, (function (SelectorEngine, EventHandler, Manipulator, BaseComponent) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var SelectorEngine__default = /*#__PURE__*/_interopDefaultLegacy(SelectorEngine);
var EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
var Manipulator__default = /*#__PURE__*/_interopDefaultLegacy(Manipulator);
var BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent);
const TRANSITION_END = 'transitionend'; // Shoutout AngusCroll (https://goo.gl/pxwQGp)
const toType = obj => {
if (obj === null || obj === undefined) {
return `${obj}`;
}
return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase();
};
const getSelector = element => {
let selector = element.getAttribute('data-bs-target');
if (!selector || selector === '#') {
let hrefAttr = element.getAttribute('href'); // The only valid content that could double as a selector are IDs or classes,
// so everything starting with `#` or `.`. If a "real" URL is used as the selector,
// `document.querySelector` will rightfully complain it is invalid.
// See https://github.com/twbs/bootstrap/issues/32273
if (!hrefAttr || !hrefAttr.includes('#') && !hrefAttr.startsWith('.')) {
return null;
} // Just in case some CMS puts out a full URL with the anchor appended
if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
hrefAttr = `#${hrefAttr.split('#')[1]}`;
}
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null;
}
return selector;
};
const getElementFromSelector = element => {
const selector = getSelector(element);
return selector ? document.querySelector(selector) : null;
};
const triggerTransitionEnd = element => {
element.dispatchEvent(new Event(TRANSITION_END));
};
const isElement = obj => {
if (!obj || typeof obj !== 'object') {
return false;
}
if (typeof obj.jquery !== 'undefined') {
obj = obj[0];
}
return typeof obj.nodeType !== 'undefined';
};
const typeCheckConfig = (componentName, config, configTypes) => {
Object.keys(configTypes).forEach(property => {
const expectedTypes = configTypes[property];
const value = config[property];
const valueType = value && isElement(value) ? 'element' : toType(value);
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`);
}
});
};
const isVisible = element => {
if (!isElement(element) || element.getClientRects().length === 0) {
return false;
}
return getComputedStyle(element).getPropertyValue('visibility') === 'visible';
};
const reflow = element => element.offsetHeight;
const getjQuery = () => {
const {
jQuery
} = window;
if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return jQuery;
}
return null;
};
const DOMContentLoadedCallbacks = [];
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
DOMContentLoadedCallbacks.forEach(callback => callback());
});
}
DOMContentLoadedCallbacks.push(callback);
} else {
callback();
}
};
const isRTL = () => document.documentElement.dir === 'rtl';
const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
const $ = getjQuery();
/* istanbul ignore if */
if ($) {
const name = plugin.NAME;
const JQUERY_NO_CONFLICT = $.fn[name];
$.fn[name] = plugin.jQueryInterface;
$.fn[name].Constructor = plugin;
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT;
return plugin.jQueryInterface;
};
}
});
};
/**
* Return the previous/next element of a list.
*
* @param {array} list The list of elements
* @param activeElement The active element
* @param shouldGetNext Choose to get next or previous element
* @param isCycleAllowed
* @return {Element|elem} The proper element
*/
const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
let index = list.indexOf(activeElement); // if the element does not exist in the list return an element depending on the direction and if cycle is allowed
if (index === -1) {
return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0];
}
const listLength = list.length;
index += shouldGetNext ? 1 : -1;
if (isCycleAllowed) {
index = (index + listLength) % listLength;
}
return list[Math.max(0, Math.min(index, listLength - 1))];
};
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.2): carousel.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'carousel';
const DATA_KEY = 'bs.carousel';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const ARROW_LEFT_KEY = 'ArrowLeft';
const ARROW_RIGHT_KEY = 'ArrowRight';
const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch
const SWIPE_THRESHOLD = 40;
const Default = {
interval: 5000,
keyboard: true,
slide: false,
pause: 'hover',
wrap: true,
touch: true
};
const DefaultType = {
interval: '(number|boolean)',
keyboard: 'boolean',
slide: '(boolean|string)',
pause: '(string|boolean)',
wrap: 'boolean',
touch: 'boolean'
};
const ORDER_NEXT = 'next';
const ORDER_PREV = 'prev';
const DIRECTION_LEFT = 'left';
const DIRECTION_RIGHT = 'right';
const KEY_TO_DIRECTION = {
[ARROW_LEFT_KEY]: DIRECTION_RIGHT,
[ARROW_RIGHT_KEY]: DIRECTION_LEFT
};
const EVENT_SLIDE = `slide${EVENT_KEY}`;
const EVENT_SLID = `slid${EVENT_KEY}`;
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`;
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`;
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`;
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`;
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`;
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`;
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`;
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`;
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
const CLASS_NAME_CAROUSEL = 'carousel';
const CLASS_NAME_ACTIVE = 'active';
const CLASS_NAME_SLIDE = 'slide';
const CLASS_NAME_END = 'carousel-item-end';
const CLASS_NAME_START = 'carousel-item-start';
const CLASS_NAME_NEXT = 'carousel-item-next';
const CLASS_NAME_PREV = 'carousel-item-prev';
const CLASS_NAME_POINTER_EVENT = 'pointer-event';
const SELECTOR_ACTIVE = '.active';
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item';
const SELECTOR_ITEM = '.carousel-item';
const SELECTOR_ITEM_IMG = '.carousel-item img';
const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev';
const SELECTOR_INDICATORS = '.carousel-indicators';
const SELECTOR_INDICATOR = '[data-bs-target]';
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]';
const POINTER_TYPE_TOUCH = 'touch';
const POINTER_TYPE_PEN = 'pen';
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class Carousel extends BaseComponent__default['default'] {
constructor(element, config) {
super(element);
this._items = null;
this._interval = null;
this._activeElement = null;
this._isPaused = false;
this._isSliding = false;
this.touchTimeout = null;
this.touchStartX = 0;
this.touchDeltaX = 0;
this._config = this._getConfig(config);
this._indicatorsElement = SelectorEngine__default['default'].findOne(SELECTOR_INDICATORS, this._element);
this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;
this._pointerEvent = Boolean(window.PointerEvent);
this._addEventListeners();
} // Getters
static get Default() {
return Default;
}
static get NAME() {
return NAME;
} // Public
next() {
this._slide(ORDER_NEXT);
}
nextWhenVisible() {
// Don't call next when the page isn't visible
// or the carousel or its parent isn't visible
if (!document.hidden && isVisible(this._element)) {
this.next();
}
}
prev() {
this._slide(ORDER_PREV);
}
pause(event) {
if (!event) {
this._isPaused = true;
}
if (SelectorEngine__default['default'].findOne(SELECTOR_NEXT_PREV, this._element)) {
triggerTransitionEnd(this._element);
this.cycle(true);
}
clearInterval(this._interval);
this._interval = null;
}
cycle(event) {
if (!event) {
this._isPaused = false;
}
if (this._interval) {
clearInterval(this._interval);
this._interval = null;
}
if (this._config && this._config.interval && !this._isPaused) {
this._updateInterval();
this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval);
}
}
to(index) {
this._activeElement = SelectorEngine__default['default'].findOne(SELECTOR_ACTIVE_ITEM, this._element);
const activeIndex = this._getItemIndex(this._activeElement);
if (index > this._items.length - 1 || index < 0) {
return;
}
if (this._isSliding) {
EventHandler__default['default'].one(this._element, EVENT_SLID, () => this.to(index));
return;
}
if (activeIndex === index) {
this.pause();
this.cycle();
return;
}
const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;
this._slide(order, this._items[index]);
} // Private
_getConfig(config) {
config = { ...Default,
...Manipulator__default['default'].getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
};
typeCheckConfig(NAME, config, DefaultType);
return config;
}
_handleSwipe() {
const absDeltax = Math.abs(this.touchDeltaX);
if (absDeltax <= SWIPE_THRESHOLD) {
return;
}
const direction = absDeltax / this.touchDeltaX;
this.touchDeltaX = 0;
if (!direction) {
return;
}
this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT);
}
_addEventListeners() {
if (this._config.keyboard) {
EventHandler__default['default'].on(this._element, EVENT_KEYDOWN, event => this._keydown(event));
}
if (this._config.pause === 'hover') {
EventHandler__default['default'].on(this._element, EVENT_MOUSEENTER, event => this.pause(event));
EventHandler__default['default'].on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event));
}
if (this._config.touch && this._touchSupported) {
this._addTouchEventListeners();
}
}
_addTouchEventListeners() {
const start = event => {
if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
this.touchStartX = event.clientX;
} else if (!this._pointerEvent) {
this.touchStartX = event.touches[0].clientX;
}
};
const move = event => {
// ensure swiping with one touch and not pinching
this.touchDeltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this.touchStartX;
};
const end = event => {
if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
this.touchDeltaX = event.clientX - this.touchStartX;
}
this._handleSwipe();
if (this._config.pause === 'hover') {
// If it's a touch-enabled device, mouseenter/leave are fired as
// part of the mouse compatibility events on first tap - the carousel
// would stop cycling until user tapped out of it;
// here, we listen for touchend, explicitly pause the carousel
// (as if it's the second time we tap on it, mouseenter compat event
// is NOT fired) and after a timeout (to allow for mouse compatibility
// events to fire) we explicitly restart cycling
this.pause();
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
}
this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval);
}
};
SelectorEngine__default['default'].find(SELECTOR_ITEM_IMG, this._element).forEach(itemImg => {
EventHandler__default['default'].on(itemImg, EVENT_DRAG_START, e => e.preventDefault());
});
if (this._pointerEvent) {
EventHandler__default['default'].on(this._element, EVENT_POINTERDOWN, event => start(event));
EventHandler__default['default'].on(this._element, EVENT_POINTERUP, event => end(event));
this._element.classList.add(CLASS_NAME_POINTER_EVENT);
} else {
EventHandler__default['default'].on(this._element, EVENT_TOUCHSTART, event => start(event));
EventHandler__default['default'].on(this._element, EVENT_TOUCHMOVE, event => move(event));
EventHandler__default['default'].on(this._element, EVENT_TOUCHEND, event => end(event));
}
}
_keydown(event) {
if (/input|textarea/i.test(event.target.tagName)) {
return;
}
const direction = KEY_TO_DIRECTION[event.key];
if (direction) {
event.preventDefault();
this._slide(direction);
}
}
_getItemIndex(element) {
this._items = element && element.parentNode ? SelectorEngine__default['default'].find(SELECTOR_ITEM, element.parentNode) : [];
return this._items.indexOf(element);
}
_getItemByOrder(order, activeElement) {
const isNext = order === ORDER_NEXT;
return getNextActiveElement(this._items, activeElement, isNext, this._config.wrap);
}
_triggerSlideEvent(relatedTarget, eventDirectionName) {
const targetIndex = this._getItemIndex(relatedTarget);
const fromIndex = this._getItemIndex(SelectorEngine__default['default'].findOne(SELECTOR_ACTIVE_ITEM, this._element));
return EventHandler__default['default'].trigger(this._element, EVENT_SLIDE, {
relatedTarget,
direction: eventDirectionName,
from: fromIndex,
to: targetIndex
});
}
_setActiveIndicatorElement(element) {
if (this._indicatorsElement) {
const activeIndicator = SelectorEngine__default['default'].findOne(SELECTOR_ACTIVE, this._indicatorsElement);
activeIndicator.classList.remove(CLASS_NAME_ACTIVE);
activeIndicator.removeAttribute('aria-current');
const indicators = SelectorEngine__default['default'].find(SELECTOR_INDICATOR, this._indicatorsElement);
for (let i = 0; i < indicators.length; i++) {
if (Number.parseInt(indicators[i].getAttribute('data-bs-slide-to'), 10) === this._getItemIndex(element)) {
indicators[i].classList.add(CLASS_NAME_ACTIVE);
indicators[i].setAttribute('aria-current', 'true');
break;
}
}
}
}
_updateInterval() {
const element = this._activeElement || SelectorEngine__default['default'].findOne(SELECTOR_ACTIVE_ITEM, this._element);
if (!element) {
return;
}
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);
if (elementInterval) {
this._config.defaultInterval = this._config.defaultInterval || this._config.interval;
this._config.interval = elementInterval;
} else {
this._config.interval = this._config.defaultInterval || this._config.interval;
}
}
_slide(directionOrOrder, element) {
const order = this._directionToOrder(directionOrOrder);
const activeElement = SelectorEngine__default['default'].findOne(SELECTOR_ACTIVE_ITEM, this._element);
const activeElementIndex = this._getItemIndex(activeElement);
const nextElement = element || this._getItemByOrder(order, activeElement);
const nextElementIndex = this._getItemIndex(nextElement);
const isCycling = Boolean(this._interval);
const isNext = order === ORDER_NEXT;
const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;
const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;
const eventDirectionName = this._orderToDirection(order);
if (nextElement && nextElement.classList.contains(CLASS_NAME_ACTIVE)) {
this._isSliding = false;
return;
}
if (this._isSliding) {
return;
}
const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName);
if (slideEvent.defaultPrevented) {
return;
}
if (!activeElement || !nextElement) {
// Some weirdness is happening, so we bail
return;
}
this._isSliding = true;
if (isCycling) {
this.pause();
}
this._setActiveIndicatorElement(nextElement);
this._activeElement = nextElement;
const triggerSlidEvent = () => {
EventHandler__default['default'].trigger(this._element, EVENT_SLID, {
relatedTarget: nextElement,
direction: eventDirectionName,
from: activeElementIndex,
to: nextElementIndex
});
};
if (this._element.classList.contains(CLASS_NAME_SLIDE)) {
nextElement.classList.add(orderClassName);
reflow(nextElement);
activeElement.classList.add(directionalClassName);
nextElement.classList.add(directionalClassName);
const completeCallBack = () => {
nextElement.classList.remove(directionalClassName, orderClassName);
nextElement.classList.add(CLASS_NAME_ACTIVE);
activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName);
this._isSliding = false;
setTimeout(triggerSlidEvent, 0);
};
this._queueCallback(completeCallBack, activeElement, true);
} else {
activeElement.classList.remove(CLASS_NAME_ACTIVE);
nextElement.classList.add(CLASS_NAME_ACTIVE);
this._isSliding = false;
triggerSlidEvent();
}
if (isCycling) {
this.cycle();
}
}
_directionToOrder(direction) {
if (![DIRECTION_RIGHT, DIRECTION_LEFT].includes(direction)) {
return direction;
}
if (isRTL()) {
return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;
}
return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;
}
_orderToDirection(order) {
if (![ORDER_NEXT, ORDER_PREV].includes(order)) {
return order;
}
if (isRTL()) {
return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;
} // Static
static carouselInterface(element, config) {
const data = Carousel.getOrCreateInstance(element, config);
let {
_config
} = data;
if (typeof config === 'object') {
_config = { ..._config,
...config
};
}
const action = typeof config === 'string' ? config : _config.slide;
if (typeof config === 'number') {
data.to(config);
} else if (typeof action === 'string') {
if (typeof data[action] === 'undefined') {
throw new TypeError(`No method named "${action}"`);
}
data[action]();
} else if (_config.interval && _config.ride) {
data.pause();
data.cycle();
}
}
static jQueryInterface(config) {
return this.each(function () {
Carousel.carouselInterface(this, config);
});
}
static dataApiClickHandler(event) {
const target = getElementFromSelector(this);
if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
return;
}
const config = { ...Manipulator__default['default'].getDataAttributes(target),
...Manipulator__default['default'].getDataAttributes(this)
};
const slideIndex = this.getAttribute('data-bs-slide-to');
if (slideIndex) {
config.interval = false;
}
Carousel.carouselInterface(target, config);
if (slideIndex) {
Carousel.getInstance(target).to(slideIndex);
}
event.preventDefault();
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
EventHandler__default['default'].on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler);
EventHandler__default['default'].on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine__default['default'].find(SELECTOR_DATA_RIDE);
for (let i = 0, len = carousels.length; i < len; i++) {
Carousel.carouselInterface(carousels[i], Carousel.getInstance(carousels[i]));
}
});
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Carousel to jQuery only if jQuery is present
*/
defineJQueryPlugin(Carousel);
return Carousel;
})));
//# sourceMappingURL=carousel.js.map

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше