diff --git a/portals/developer-portal/CLAUDE.md b/portals/developer-portal/CLAUDE.md new file mode 100644 index 0000000000..d26552f54c --- /dev/null +++ b/portals/developer-portal/CLAUDE.md @@ -0,0 +1,137 @@ +# Developer Portal — Claude Code Guide + +## Project overview + +Express.js + Handlebars (`.hbs`) server-rendered developer portal for WSO2 API Manager. +Two distinct UI surfaces share the same server: + +| Surface | Templates | CSS | +|---|---|---| +| **Developer portal** (public-facing) | `src/defaultContent/pages/` | `src/defaultContent/styles/` | +| **Portal management / admin** | `src/pages/` | `src/styles/` | + +These are separate concerns — do not mix their CSS files. + +## CSS architecture + +### Developer portal (`src/defaultContent/styles/`) + +``` +main.css + ├── @import components.css ← canonical shared component library + ├── @import home.css + ├── @import footer.css + ├── @import header.css + ├── @import api-listing.css + ├── @import api-content.css + ├── @import api-landing.css + ├── @import doc.css + ├── @import side-bar.css + └── @import default-api.css +``` + +**Standalone page CSS files** (loaded on their own route, not via `main.css`) must start with: +```css +@import "/styles/components.css"; +``` +Affected files: `subscriptions.css`, `login.css`, `os.css`. +This means `components.css` classes (`.page-header`, `.dp-btn`, `.dp-breadcrumb`, `.dp-empty`, etc.) are always available everywhere. + +### Never touch +`src/defaultContent/styles/async-tryout.css` and `src/styles/async-tryout.css` — third-party bundles. + +## Design tokens (CSS custom properties) + +Defined in `main.css` `:root`: + +```css +--wso2-gradient: linear-gradient(135deg, #ef4223 0%, #ff8636 100%); +--primary-gradient: linear-gradient(135deg, #3d6b8a, #1c3b52); +--primary-main-color: #1A4C6D; +--border-radius: 0.5rem; +--font-family-sans: "Montserrat", "Quicksand", "Noto Sans", "Poppins", sans-serif; +``` + +## CSS conventions + +- **Units**: `rem` for all sizes (divide px by 16). Exception: `border-width` and `box-shadow` stay in `px`. +- **Component prefix**: shared components use `dp-*` (e.g. `dp-btn`, `dp-breadcrumb`, `dp-empty`). +- **No debug borders**: never commit `border: 1px solid red`. + +## Canonical shared components (`components.css`) + +### Page header +```css +.page-header /* flex row, space-between, gap: 16px, margin-bottom: 26px */ +.page-title /* 1.5rem, font-weight: 700, color: #1a2433 */ +.page-desc /* 0.875rem, line-height: 1.5, color: #637282 */ +``` +Use these global classes everywhere. Do not invent page-specific header classes (e.g. `sub-page-header`, `apps-page-header` are legacy — migrate away when touching those pages). + +### Breadcrumb +```hbs + +``` +Top-level pages (APIs, MCP Servers, Subscriptions, API Workflows) have no parent — omit breadcrumb there. + +### Buttons +```css +.dp-btn /* base */ +.dp-btn--primary /* wso2-gradient fill */ +.dp-btn--secondary /* outlined */ +.dp-btn--icon /* square icon-only */ +``` + +### Empty state +```css +.dp-empty /* flex column, centered, padding: 3.75rem 0 5rem, NO border */ +.dp-empty-icon +.dp-empty-title +.dp-empty-desc +``` + +## Hard rules + +1. **Unimplemented action buttons** must always have: + ```html + disabled title="Backend not wired yet — UI preview only" + ``` + +2. **Do not change any file without asking first** unless it's the file directly under discussion. + +3. **Breadcrumb on sub-pages**: all API/MCP detail sub-pages (subscriptions, keys, docs, flows detail) must have a breadcrumb. + +## Handlebars template patterns + +- `{{baseUrl}}` — portal base URL available in all templates +- `{{apiMetadata.apiHandle}}` — API/MCP handle (slug) for URL construction +- `{{or apiMetadata.apiInfo.apiTitle apiMetadata.apiInfo.apiName}}` — display name pattern (title with name fallback) +- `{{apiName}}` — passed explicitly to docs pages from `apiContentController.js` +- `{{baseDocUrl}}` — API/MCP detail page URL, passed explicitly to docs pages + +## Controller locations + +| Controller | File | +|---|---| +| API/MCP docs pages | `src/controllers/apiContentController.js` | +| Application management views | `src/controllers/viewConfigureController.js` | + +`loadDocsPage` and `loadDocument` in `apiContentController.js` each have a design-mode and non-design-mode path — template context variables must be added in all four locations. + +## Key page files + +| Page | Template | CSS | +|---|---|---| +| API listing | `src/defaultContent/pages/apis/page.hbs` | `api-listing.css` | +| API detail | `src/defaultContent/pages/api-landing/page.hbs` | `api-landing.css` | +| API subscriptions | `src/defaultContent/pages/api-subscriptions/page.hbs` | `api-content.css` | +| API keys | `src/defaultContent/pages/api-keys/page.hbs` | `api-content.css` | +| MCP landing | `src/defaultContent/pages/mcp-landing/page.hbs` | `api-landing.css` | +| Docs | `src/defaultContent/pages/docs/page.hbs` | `doc.css` | +| Subscriptions | `src/defaultContent/pages/subscriptions/page.hbs` | `subscriptions.css` | +| API Flows | `src/defaultContent/pages/api-flows/page.hbs` | (via main.css) | +| Applications | `src/pages/applications/partials/applications-listing.hbs` | `src/styles/applications.css` | diff --git a/portals/developer-portal/docker-compose.platform-api.yaml b/portals/developer-portal/docker-compose.platform-api.yaml new file mode 100644 index 0000000000..09682d73ab --- /dev/null +++ b/portals/developer-portal/docker-compose.platform-api.yaml @@ -0,0 +1,51 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# -------------------------------------------------------------------- + +# Platform API only — for local development with npm start +# +# Quick start: +# 1. docker compose -f docker-compose.platform-api.yaml up -d +# 2. npm start +# 3. Open http://localhost:3000 +# Login: admin / admin +# +# Set AUTH_JWT_SECRET_KEY in your shell or a .env file to keep sessions alive +# across Platform API restarts. When unset a random key is generated at startup. + +services: + platform-api: + image: ghcr.io/wso2/api-platform/platform-api:0.10.0 + container_name: platform-api + restart: unless-stopped + command: ["-config", "/etc/platform-api/config-platform-api.toml"] + environment: + - AUTH_JWT_SECRET_KEY=${AUTH_JWT_SECRET_KEY:-} + volumes: + - ./configs/config-platform-api.toml:/etc/platform-api/config-platform-api.toml:ro + - platform-api-data:/app/data + ports: + - "9243:9243" + healthcheck: + test: ["CMD", "curl", "-fk", "https://localhost:9243/health"] + interval: 30s + timeout: 5s + start_period: 20s + retries: 3 + +volumes: + platform-api-data: diff --git a/portals/developer-portal/nodemon.json b/portals/developer-portal/nodemon.json new file mode 100644 index 0000000000..823816e88a --- /dev/null +++ b/portals/developer-portal/nodemon.json @@ -0,0 +1,8 @@ +{ + "ext": "js,hbs,yaml,json,css", + "ignore": [ + "devportal.db", + "node_modules/", + "*.test.js" + ] +} diff --git a/portals/developer-portal/package.json b/portals/developer-portal/package.json index 93a73eb7d4..09f8df1a71 100644 --- a/portals/developer-portal/package.json +++ b/portals/developer-portal/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "design-mode": "npm run build-css --watch & nodemon src/dev-server.js", - "start": "npm rebuild better-sqlite3 --ignore-scripts=false && nodemon src/server.js", + "start": "npm rebuild better-sqlite3 --ignore-scripts=false && NODE_ENV=development nodemon src/server.js", "debug": "nodemon --inspect src/server.js", "multi-tenant": "npm run build-css --watch & nodemon src/multi-tenant.js", "build-css": "node watcher.js", diff --git a/portals/developer-portal/src/app.js b/portals/developer-portal/src/app.js index d8af8995ba..520e4fbf44 100644 --- a/portals/developer-portal/src/app.js +++ b/portals/developer-portal/src/app.js @@ -123,6 +123,11 @@ configurePassport(SERVER_ID); app.use(constants.ROUTE.TECHNICAL_STYLES, express.static(path.join(require.main.filename, '../styles'))); app.use(constants.ROUTE.TECHNICAL_SCRIPTS, express.static(path.join(require.main.filename, '../scripts'))); +// Dev live-reload SSE endpoint — must be registered before org-resolution routes +if (process.env.NODE_ENV === 'development') { + require('./liveReload').setup(app); +} + // Redirect unrecognised root-level paths (e.g. /robots.txt, /sitemap.xml) before // the /:orgName route can treat them as org IDs. app.use((req, res, next) => { diff --git a/portals/developer-portal/src/controllers/apiContentController.js b/portals/developer-portal/src/controllers/apiContentController.js index 199547c1e1..3157ad55ed 100644 --- a/portals/developer-portal/src/controllers/apiContentController.js +++ b/portals/developer-portal/src/controllers/apiContentController.js @@ -35,6 +35,7 @@ const apiMetadataService = require('../services/apiMetadataService'); const { apiUsesApiKeySecurity, findSubscriptionTokenHeader } = require('../utils/apiDefinitionUtil'); const sampleApiLoader = require('../utils/sampleApiLoader'); const adminService = require('../services/adminService'); +const { seedSampleAPIs } = require('../services/sampleSeederService'); const apiFlowService = require('../services/apiFlowService'); const { buildSchema, getIntrospectionQuery, graphql: executeGraphQL } = require('graphql'); const yaml = require('js-yaml'); @@ -505,8 +506,10 @@ const loadDocsPage = async (req, res) => { const templateContent = { apiMD: '', baseUrl: config.baseUrl + constants.ROUTE.VIEWS_PATH + viewName + '/api/' + apiHandle, + baseDocUrl: config.baseUrl + constants.ROUTE.VIEWS_PATH + viewName + '/api/' + apiHandle, docTypes: docNames, apiType: apiMetadata.apiInfo?.apiType, + apiName: apiMetadata.apiInfo?.apiName || '', showApiKeysNav: apiUsesApiKeySecurity(metaForNav), } html = renderTemplate(layoutPath + 'pages/docs/page.hbs', layoutPath + 'layout/main.hbs', templateContent, false); @@ -554,8 +557,10 @@ const loadDocsPage = async (req, res) => { const templateContent = { baseUrl: '/' + orgName + '/views/' + viewName + "/api/" + apiHandle, + baseDocUrl: '/' + orgName + '/views/' + viewName + "/api/" + apiHandle, docTypes: docNames, apiType: apiType, + apiName: apiMetadata[0].dataValues.API_NAME || '', profile: req.isAuthenticated() ? profile : null, devportalMode: devportalMode, showApiKeysNav: apiUsesApiKeySecurity(metaForNav, apiDefinitionForNav), @@ -623,6 +628,9 @@ const loadDocument = async (req, res) => { templateContent.baseUrl = config.baseUrl + constants.ROUTE.VIEWS_PATH + viewName; templateContent.baseDocUrl = config.baseUrl + constants.ROUTE.VIEWS_PATH + viewName + '/api/' + apiHandle; templateContent.docTypes = metaData.docTypes; + templateContent.currentDocName = docName || null; + templateContent.currentDocType = docType || null; + templateContent.apiName = metaData.apiInfo?.apiName || ''; const metaForNav = { apiInfo: { gatewayType: metaData.apiInfo?.gatewayType }, apiReferenceID: metaData.apiReferenceID }; templateContent.showApiKeysNav = apiUsesApiKeySecurity(metaForNav); const html = renderTemplate(layoutPath + 'pages/docs/page.hbs', layoutPath + 'layout/main.hbs', templateContent, false); @@ -751,6 +759,9 @@ const loadDocument = async (req, res) => { templateContent.baseUrl = '/' + orgName + constants.ROUTE.VIEWS_PATH + viewName; templateContent.baseDocUrl = baseDocUrl; templateContent.docTypes = docNames; + templateContent.currentDocName = docName || null; + templateContent.currentDocType = docType || null; + templateContent.apiName = apiMetadata[0].dataValues.API_NAME || ''; let profile = null; if (req.user) { profile = { @@ -1493,6 +1504,25 @@ const loadDocumentMd = async (req, res) => { } }; +const seedSamples = async (req, res) => { + const { orgName } = req.params; + if (!req.user?.isAdmin) { + return res.status(403).json({ error: 'Access denied' }); + } + try { + const orgDetails = await orgDao.get(orgName); + const results = await seedSampleAPIs(orgDetails.ORG_ID); + const deployed = results.filter(r => r.status === 'ok').length; + const skipped = results.filter(r => r.status === 'exists').length; + const failed = results.filter(r => r.status === 'failed').length; + logger.info('Sample seed complete', { orgName, deployed, skipped, failed }); + res.json({ results, deployed, skipped, failed }); + } catch (err) { + logger.error('Sample seed error', { orgName, error: err.message }); + res.status(500).json({ error: err.message }); + } +}; + module.exports = { loadAPIs, loadAPIContent, @@ -1505,4 +1535,5 @@ module.exports = { loadAPIContentMd, loadDocumentMd, loadSpecificationRaw: loadAPIDefinitionRaw, + seedSamples, }; diff --git a/portals/developer-portal/src/controllers/orgContentController.js b/portals/developer-portal/src/controllers/orgContentController.js index ef8a2f183e..3ee35c1f6a 100644 --- a/portals/developer-portal/src/controllers/orgContentController.js +++ b/portals/developer-portal/src/controllers/orgContentController.js @@ -89,7 +89,8 @@ const loadOrgContentFromAPI = async (req, res) => { templateContent = { devportalMode: devportalMode, baseUrl: '/' + orgName + constants.ROUTE.VIEWS_PATH + req.params.viewName, - profile: req.isAuthenticated() ? profile : null + profile: req.isAuthenticated() ? profile : null, + showOnboarding: !!(profile?.isAdmin), }; html = await renderTemplateFromAPI(templateContent, orgId, orgName, 'pages/home', req.params.viewName); // Track home page visit telemetry diff --git a/portals/developer-portal/src/controllers/viewConfigureController.js b/portals/developer-portal/src/controllers/viewConfigureController.js index c795175efe..ad7d5cc628 100644 --- a/portals/developer-portal/src/controllers/viewConfigureController.js +++ b/portals/developer-portal/src/controllers/viewConfigureController.js @@ -21,6 +21,8 @@ const logger = require('../config/logger'); const orgDao = require('../dao/organizationDao'); const apiDao = require('../dao/apiDao'); const viewDao = require('../dao/viewDao'); +const labelDao = require('../dao/labelDao'); +const subscriptionPolicyDao = require('../dao/subscriptionPolicyDao'); const apiFlowService = require('../services/apiFlowService'); const { renderGivenTemplate, loadLayoutFromAPI } = require('../utils/util'); const { getSessionCsrfToken } = require('../middlewares/csrfProtection'); @@ -54,17 +56,49 @@ const loadViewSettingsPage = async (req, res) => { const apiFlows = await apiFlowService.getAllAPIFlowsFromDB(orgID, viewId); templateContent.apiFlows = apiFlows; - const allAPIs = await apiDao.getByCondition({ ORG_ID: orgID, STATUS: constants.API_STATUS.PUBLISHED }); + const allAPIs = await apiDao.getByCondition({ ORG_ID: orgID }); templateContent.orgAPIs = allAPIs.map(api => ({ apiId: api.API_ID, apiName: api.API_NAME, apiHandle: api.API_HANDLE, apiDescription: api.API_DESCRIPTION, apiType: api.API_TYPE, + apiVersion: api.API_VERSION, + apiStatus: api.STATUS, productionUrl: api.PRODUCTION_URL, - agentVisibility: api.AGENT_VISIBILITY + sandboxUrl: api.SANDBOX_URL, + provider: api.PROVIDER, + gatewayType: api.GATEWAY_TYPE, + tags: api.TAGS || '', + agentVisibility: api.AGENT_VISIBILITY, + subscriptionPolicies: (api.SubscriptionPolicies || []).map(p => p.POLICY_NAME), })); + let orgLabels = []; + try { + const labelsRaw = await labelDao.list(orgID); + orgLabels = labelsRaw.map(l => ({ labelId: l.LABEL_ID, name: l.NAME, displayName: l.DISPLAY_NAME })); + } catch (err) { + logger.warn('Failed to load labels for settings page', { error: err.message }); + } + templateContent.orgLabels = orgLabels; + + let orgPolicies = []; + try { + const policiesRaw = await subscriptionPolicyDao.list(orgID); + orgPolicies = policiesRaw.map(p => ({ + policyId: p.POLICY_ID, + policyName: p.POLICY_NAME, + displayName: p.DISPLAY_NAME, + description: p.DESCRIPTION || '', + requestCount: p.REQUEST_COUNT, + refId: p.REF_ID || '', + })); + } catch (err) { + logger.warn('Failed to load subscription policies for settings page', { error: err.message }); + } + templateContent.orgPolicies = orgPolicies; + const configAsset = await orgDao.getContent({ orgId: orgID, fileType: constants.FILE_TYPE.LLMS_CONFIG, viewName, fileName: constants.FILE_NAME.LLMS_CONFIG }); diff --git a/portals/developer-portal/src/defaultContent/images/devportal-logo.png b/portals/developer-portal/src/defaultContent/images/devportal-logo.png index ae6ac6b1c2..6b7bda2f92 100644 Binary files a/portals/developer-portal/src/defaultContent/images/devportal-logo.png and b/portals/developer-portal/src/defaultContent/images/devportal-logo.png differ diff --git a/portals/developer-portal/src/defaultContent/images/devportal-logo.svg b/portals/developer-portal/src/defaultContent/images/devportal-logo.svg new file mode 100644 index 0000000000..e972ab512b --- /dev/null +++ b/portals/developer-portal/src/defaultContent/images/devportal-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/portals/developer-portal/src/defaultContent/images/favicon.ico b/portals/developer-portal/src/defaultContent/images/favicon.ico new file mode 100644 index 0000000000..fafb3e61cc Binary files /dev/null and b/portals/developer-portal/src/defaultContent/images/favicon.ico differ diff --git a/portals/developer-portal/src/defaultContent/images/mcp-icon.svg b/portals/developer-portal/src/defaultContent/images/mcp-icon.svg new file mode 100644 index 0000000000..3be9fb4bab --- /dev/null +++ b/portals/developer-portal/src/defaultContent/images/mcp-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/portals/developer-portal/src/defaultContent/layout/main.hbs b/portals/developer-portal/src/defaultContent/layout/main.hbs index ab8770ef12..890f29eef0 100644 --- a/portals/developer-portal/src/defaultContent/layout/main.hbs +++ b/portals/developer-portal/src/defaultContent/layout/main.hbs @@ -5,7 +5,7 @@ DevPortal - + @@ -27,18 +27,20 @@ devportalMode=devportalMode showApiWorkflowsNav=showApiWorkflowsNav }} - {{> sidebar - profile=profile - baseUrl=baseUrl - devportalMode=devportalMode - showApiKeysNav=showApiKeysNav - showApiWorkflowsNav=showApiWorkflowsNav - }} -
-
- {{{ body }}} -
-
+
+ {{> sidebar + profile=profile + baseUrl=baseUrl + devportalMode=devportalMode + showApiKeysNav=showApiKeysNav + showApiWorkflowsNav=showApiWorkflowsNav + }} +
+
+ {{{ body }}} +
+
+
{{> footer }} {{!-- Global Delete Confirmation Modal --}} @@ -49,6 +51,27 @@ {{{slots.scripts}}} + diff --git a/portals/developer-portal/src/defaultContent/pages/api-flows/detail/page.hbs b/portals/developer-portal/src/defaultContent/pages/api-flows/detail/page.hbs index b54c36e998..7519ac2a05 100644 --- a/portals/developer-portal/src/defaultContent/pages/api-flows/detail/page.hbs +++ b/portals/developer-portal/src/defaultContent/pages/api-flows/detail/page.hbs @@ -19,11 +19,12 @@
- {{!-- Back Button --}} - - - Back - + {{!-- Breadcrumb --}} + {{!-- Header --}}
diff --git a/portals/developer-portal/src/defaultContent/pages/api-flows/page.hbs b/portals/developer-portal/src/defaultContent/pages/api-flows/page.hbs index 1094af4b59..12bdea76db 100644 --- a/portals/developer-portal/src/defaultContent/pages/api-flows/page.hbs +++ b/portals/developer-portal/src/defaultContent/pages/api-flows/page.hbs @@ -3,109 +3,76 @@ * * API Flows Gallery --> -{{#pageHead}} - - - -{{/pageHead}} -{{#pageScripts}} - -{{/pageScripts}} -
- {{!-- Hero --}} -
-
-
-

API Workflows

-

Pre-configured workflows. Let your AI agents reference these best practices to use your APIs correctly, every time—no hallucination, no manual setup

-
- {{#if profile.isAdmin}} - - Manage Workflows - - {{/if}} -
-
- - {{!-- Flows Grid --}} - {{#if apiFlows.length}} -
- {{#each apiFlows}} -
-
-

{{this.name}}

-
-
-

{{this.description}}

- {{#if this.sourcesPreview.length}} -
- Powered by -
- {{#each this.sourcesPreview}} - {{this.name}} - {{/each}} - {{#if sourcesMoreCount}} - +{{sourcesMoreCount}} more - {{/if}} -
-
- {{/if}} -
- -
- {{/each}} -
- {{else}} -
- -

No API Workflows Available

-

Check back soon for published API workflows.

-
- {{/if}} +
+ - {{!-- Prompt Modal --}} -
-
-
-

-
- - - - -
-
-
-

-            
+ {{#if apiFlows.length}} +
+ {{#each apiFlows}} +
+
+

{{this.name}}

+
+
+

{{this.description}}

+ {{#if this.sourcesPreview.length}} +
POWERED BY
+
+ {{#each this.sourcesPreview}} + {{this.name}} + {{/each}} + {{#if ../sourcesMoreCount}} + +{{../sourcesMoreCount}} more + {{/if}}
+ {{/if}} +
+
+ {{/each}} +
+ {{else}} +
+ + + +

No workflows yet

+

Create a workflow to encode a multi-step API sequence your AI agents can follow. Published workflows will appear here for every consumer.

+ {{#if profile.isAdmin}} + + Create workflow + + {{/if}} +
+ {{/if}} - {{> alert}} + {{> alert}} +
diff --git a/portals/developer-portal/src/defaultContent/pages/api-keys/page.hbs b/portals/developer-portal/src/defaultContent/pages/api-keys/page.hbs index 3a624c7643..f8821070d7 100644 --- a/portals/developer-portal/src/defaultContent/pages/api-keys/page.hbs +++ b/portals/developer-portal/src/defaultContent/pages/api-keys/page.hbs @@ -10,40 +10,51 @@ * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR ANY + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. --> {{#pageHead}} - - + {{/pageHead}} {{#pageScripts}} - + {{/pageScripts}} -
-
-
- API Keys - {{#unless isReadOnlyMode}} - {{#unless apiKeysLoadError}} - - {{/unless}} - {{/unless}} -
-
-
- {{> api-key-list }} -
-
- {{> warning modalTitle="Confirm" modalMessage="" modalFunction="" }} - {{> alert }} + + + + + + + +{{> api-key-list }} + +{{> alert }} diff --git a/portals/developer-portal/src/defaultContent/pages/api-keys/partials/api-key-list.hbs b/portals/developer-portal/src/defaultContent/pages/api-keys/partials/api-key-list.hbs index 33e65279a8..430a1866c9 100644 --- a/portals/developer-portal/src/defaultContent/pages/api-keys/partials/api-key-list.hbs +++ b/portals/developer-portal/src/defaultContent/pages/api-keys/partials/api-key-list.hbs @@ -1,148 +1,158 @@ {{#if apiKeysLoadError}} -