Contoso sample V2
This commit is contained in:
Родитель
f1aefa0cb5
Коммит
4a912f4485
25
README.md
25
README.md
|
@ -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
Двоичные данные
ReadmeFiles/Purchaser10.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 199 KiB После Ширина: | Высота: | Размер: 156 KiB |
Двоичные данные
ReadmeFiles/Purchaser9.png
Двоичные данные
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/).
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
5001
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
поставляемый
Normal file
5001
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
поставляемый
Normal file
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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 */
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
423
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
поставляемый
Normal file
423
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
поставляемый
Normal file
|
@ -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 */
|
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
8
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
поставляемый
Normal file
8
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
поставляемый
Normal file
|
@ -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 */
|
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
4752
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
поставляемый
Normal file
4752
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
поставляемый
Normal file
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
4743
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
поставляемый
Normal file
4743
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
поставляемый
Normal file
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
поставляемый
Normal file
7
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
поставляемый
Normal file
1
src/CommandCenter/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
поставляемый
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
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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
|
|
@ -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
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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
|
|
@ -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
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче