diff --git a/.gitignore b/.gitignore index 6dd55eb..57f6c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # dependencies /node_modules + /dist +cpToken.json /.pnp .pnp.js @@ -16,7 +18,6 @@ # production /build -/src/styles /src/.tmp /src/config/config.js diff --git a/config.json b/config.json index 1e6baca..4d48e0d 100644 --- a/config.json +++ b/config.json @@ -1,12 +1,13 @@ { "port": 3000, - "pathToContent": "../src/", - "mode": "development", + "pathToContent": "../api-developer-portal/src/", + "mode": "production", "db": { "username": "postgres", "password": "postgres", - "database": "DEVPORTAL_NEWSCHEMA", + "database": "devportal", "host": "localhost", "dialect": "postgres" - } + }, + "controlPlaneUrl": "https://localhost:9443/api/am/devportal/v3.1" } diff --git a/package-lock.json b/package-lock.json index b624919..51e059e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@asyncapi/react-component": "^2.3.4", + "axios": "^1.7.7", "bootstrap-icons": "^1.11.3", "chokidar": "^3.6.0", "crypto": "^1.0.1", @@ -21,6 +22,7 @@ "graphql": "^16.9.0", "handlebars": "^4.7.8", "hbs": "^4.2.0", + "https": "^1.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "marked": "^13.0.3", @@ -2546,6 +2548,31 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4049,6 +4076,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4720,6 +4767,12 @@ "npm": ">=1.3.7" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -6885,6 +6938,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index ebb4f87..c6fb547 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "homepage": "https://github.com/api-developer-portal-core#readme", "dependencies": { "@asyncapi/react-component": "^2.3.4", + "axios": "^1.7.7", "bootstrap-icons": "^1.11.3", "chokidar": "^3.6.0", "crypto": "^1.0.1", @@ -37,6 +38,7 @@ "graphql": "^16.9.0", "handlebars": "^4.7.8", "hbs": "^4.2.0", + "https": "^1.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "marked": "^13.0.3", @@ -66,7 +68,8 @@ "**/*.js" ], "assets": [ - "src/pages/**/*" + "src/pages/**/*", + "src/styles/**/*" ], "targets": [ "node18-macos-x64", diff --git a/src/app.js b/src/app.js index e3257ec..7a2ca36 100644 --- a/src/app.js +++ b/src/app.js @@ -26,6 +26,7 @@ const authRoute = require('./routes/authRoute'); const devportalRoute = require('./routes/devportalRoute'); const orgContent = require('./routes/orgContentRoute'); const apiContent = require('./routes/apiContentRoute'); +const applicationContent = require('./routes/applicationsContentRoute'); const customContent = require('./routes/customPageRoute'); const config = require(process.cwd() + '/config.json'); const Handlebars = require('handlebars'); @@ -76,6 +77,8 @@ passport.deserializeUser((user, done) => { app.use(constants.ROUTE.STYLES, express.static(path.join(process.cwd(), filePrefix + 'styles'))); app.use(constants.ROUTE.IMAGES, express.static(path.join(process.cwd(), filePrefix + 'images'))); +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'))); //backend routes app.use(constants.ROUTE.DEV_PORTAL, devportalRoute); @@ -86,6 +89,7 @@ if (config.mode === constants.DEV_MODE) { } else { app.use(constants.ROUTE.DEFAULT, authRoute); app.use(constants.ROUTE.DEFAULT, apiContent); + app.use(constants.ROUTE.DEFAULT, applicationContent); app.use(constants.ROUTE.DEFAULT, orgContent); app.use(constants.ROUTE.DEFAULT, customContent); } diff --git a/src/controllers/apiContentController.js b/src/controllers/apiContentController.js index 69ef7cb..287935c 100644 --- a/src/controllers/apiContentController.js +++ b/src/controllers/apiContentController.js @@ -37,8 +37,8 @@ const loadAPIs = async (req, res) => { const templateContent = { apiMetadata: await loadAPIMetaDataList(), baseUrl: constants.BASE_URL + config.port - }; - html = renderTemplate(filePrefix + 'pages/apis/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + } + html = renderTemplate(filePrefix + 'pages/apis/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false); } else { try { const organization = await adminDao.getOrganization(orgName); @@ -54,8 +54,8 @@ const loadAPIs = async (req, res) => { console.log("Rendering default api listing page from file"); const templateContent = { baseUrl: constants.BASE_URL + config.port - }; - html = renderTemplate(filePrefix + 'pages/apis/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + } + html = renderTemplate(filePrefix + 'pages/apis/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false); } } res.send(html); @@ -78,8 +78,8 @@ const loadAPIContent = async (req, res) => { apiMetadata: metaData, baseUrl: constants.BASE_URL + config.port, schemaUrl: orgName + '/mock/' + apiName + '/apiDefinition.xml' - }; - html = renderTemplate(filePrefix + 'pages/api-landing/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + } + html = renderTemplate(filePrefix + 'pages/api-landing/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false) } else { try { const organization = await adminDao.getOrganization(orgName); @@ -116,8 +116,8 @@ const loadTryOutPage = async (req, res) => { baseUrl: constants.BASE_URL + config.port, apiType: metaData.apiInfo.apiType, swagger: apiDefinition - }; - html = renderTemplate('../pages/tryout/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + } + html = renderTemplate('../pages/tryout/page.hbs', filePrefix + 'layout/main.hbs', templateContent, true); } else { try { const organization = await adminDao.getOrganization(orgName); diff --git a/src/controllers/applicationsContentController.js b/src/controllers/applicationsContentController.js new file mode 100644 index 0000000..3c11f06 --- /dev/null +++ b/src/controllers/applicationsContentController.js @@ -0,0 +1,303 @@ +const { renderTemplate } = require('../utils/util'); +const config = require(process.cwd() + '/config'); +const cpToken = require(process.cwd() + '/cpToken'); +const constants = require('../utils/constants'); +const path = require('path'); +const fs = require('fs'); +const axios = require('axios'); +const https = require('https'); + +const filePrefix = config.pathToContent; +const controlPlaneUrl = config.controlPlaneUrl; +const token = cpToken.token; + +// Create an HTTPS agent that bypasses certificate verification +// Will remove in production +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +// ***** Load Applications ***** + +const loadApplications = async (req, res) => { + const orgName = req.params.orgName; + let html, metaData, templateContent; + if (config.mode === constants.DEV_MODE) { + metaData = await getMockApplications(); + templateContent = { + applicationsMetadata: metaData, + baseUrl: constants.BASE_URL + config.port + } + } + else { + console.log('/' + orgName); + metaData = await getAPIMApplications(); + templateContent = { + applicationsMetadata: metaData, + baseUrl: '/' + orgName + } + } + html = renderTemplate('../pages/applications/page.hbs', filePrefix + 'layout/main.hbs', templateContent, true); + res.send(html); +} + +async function getMockApplications() { + const mockApplicationsMetaDataPath = path.join(process.cwd(), filePrefix + '../mock/Applications', 'applications.json'); + const mockApplicationsMetaData = JSON.parse(fs.readFileSync(mockApplicationsMetaDataPath, 'utf-8')); + return mockApplicationsMetaData.list; +} + +async function getAPIMApplications() { + try { + const response = await axios({ + method: 'GET', + url: controlPlaneUrl + '/applications', + headers: { + Authorization: `Bearer ${token}`, + }, + httpsAgent + }); + return response.data.list; + } catch (error) { + console.error('Error fetching applications:', error.message); + } +} + +// ***** Load Throttling Policies ***** + +const loadThrottlingPolicies = async (req, res) => { + const orgName = req.params.orgName; + let html, metaData, templateContent; + if (config.mode === constants.DEV_MODE) { + metaData = await getMockThrottlingPolicies(); + templateContent = { + throttlingPoliciesMetadata: metaData, + baseUrl: constants.BASE_URL + config.port + } + } + else { + metaData = await getAPIMThrottlingPolicies(); + templateContent = { + throttlingPoliciesMetadata: metaData, + baseUrl: '/' + orgName + } + } + html = renderTemplate('../pages/add-application/page.hbs', filePrefix + 'layout/main.hbs', templateContent, true); + res.send(html); +} + +async function getMockThrottlingPolicies() { + const mockThrottlingPoliciesMetaDataPath = path.join(process.cwd(), filePrefix + '../mock/Applications', 'throttlingPolicies.json'); + const mockThrottlingPoliciesMetaData = JSON.parse(fs.readFileSync(mockThrottlingPoliciesMetaDataPath, 'utf-8')); + return mockThrottlingPoliciesMetaData.list; +} + +async function getAPIMThrottlingPolicies() { + try { + const response = await axios({ + method: 'GET', + url: controlPlaneUrl + '/throttling-policies/application', + headers: { + Authorization: `Bearer ${token}`, + }, + httpsAgent + }); + return response.data.list; + } catch (error) { + console.error('Error fetching throttling policies:', error.message); + } +} + +// ***** Load Application ***** + +const loadApplication = async (req, res) => { + const orgName = req.params.orgName; + const applicationId = req.params.applicationid; + let html, templateContent, metaData; + if (config.mode === constants.DEV_MODE) { + metaData = await getMockApplication(); + templateContent = { + applicationMetadata: metaData, + baseUrl: constants.BASE_URL + config.port + } + } else { + metaData = await getAPIMApplication(applicationId); + templateContent = { + applicationMetadata: metaData, + baseUrl: '/' + orgName + } + } + html = renderTemplate('../pages/application/page.hbs', filePrefix + 'layout/main.hbs', templateContent, true); + res.send(html); +} + +const loadApplicationForEdit = async (req, res) => { + const orgName = req.params.orgName; + const applicationId = req.params.applicationid; + let html, templateContent, metaData; + if (config.mode === constants.DEV_MODE) { + metaData = await getMockApplication(); + throttlingMetaData = await getMockThrottlingPolicies(); + templateContent = { + applicationMetadata: metaData, + throttlingPoliciesMetadata: throttlingMetaData, + baseUrl: constants.BASE_URL + config.port + } + } else { + metaData = await getAPIMApplication(applicationId); + throttlingMetaData = await getAPIMThrottlingPolicies(); + templateContent = { + applicationMetadata: metaData, + throttlingPoliciesMetadata: throttlingMetaData, + baseUrl: '/' + orgName + } + } + html = renderTemplate('../pages/edit-application/page.hbs', filePrefix + 'layout/main.hbs', templateContent, true); + res.send(html); +} + +async function getMockApplication() { + const mockApplicationMetaDataPath = path.join(process.cwd(), filePrefix + '../mock/Applications/DefaultApplication', 'DefaultApplication.json'); + const mockApplicationMetaData = JSON.parse(fs.readFileSync(mockApplicationMetaDataPath, 'utf-8')); + return mockApplicationMetaData; +} + +async function getAPIMApplication(applicationId) { + try { + const response = await axios({ + method: 'GET', + url: controlPlaneUrl + '/applications/' + applicationId, + headers: { + Authorization: `Bearer ${token}`, + }, + httpsAgent + }); + return response.data; + } catch (error) { + console.error('Error fetching application:', error.message); + } +} + +// ***** POST / DELETE / PUT Functions ***** (Only work in production) + +// ***** Save Application ***** + +const saveApplication = async (req, res) => { + try { + const { name, throttlingPolicy, description } = req.body; + const response = await axios.post( + `${controlPlaneUrl}/applications`, + { + name, + throttlingPolicy, + description, + tokenType: 'JWT', + groups: [], + attributes: {}, + subscriptionScopes: [] + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + httpsAgent, + } + ); + res.status(200).json({ message: response.data.message }); + } catch (error) { + console.error('Error saving application:', error.message); + res.status(500).json({ error: 'Failed to save application' }); + } +}; + +// ***** Update Application ***** + +const updateApplication = async (req, res) => { + try { + const { name, throttlingPolicy, description } = req.body; + const applicationId = req.params.applicationid; + const response = await axios.put( + `${controlPlaneUrl}/applications/${applicationId}`, + { + name, + throttlingPolicy, + description, + tokenType: 'JWT', + groups: [], + attributes: {}, + subscriptionScopes: [] + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + httpsAgent, + } + ); + res.status(200).json({ message: response.data.message }); + } catch (error) { + console.error('Error updating application:', error.message); + res.status(500).json({ error: 'Failed to update application' }); + } +}; + +// ***** Delete Application ***** + +const deleteApplication = async (req, res) => { + try { + const applicationId = req.params.applicationid; + const response = await axios.delete( + `${controlPlaneUrl}/applications/${applicationId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + httpsAgent, + } + ); + res.status(200).json({ message: response.data.message }); + } catch (error) { + console.error('Error deleting application:', error.message); + res.status(500).json({ error: 'Failed to delete application' }); + } +} + +// ***** Save Application ***** + +const resetThrottlingPolicy = async (req, res) => { + try { + const applicationId = req.params.applicationid; + const { userName } = req.body; + const response = await axios.post( + `${controlPlaneUrl}/applications/${applicationId}/reset-throttle-policy`, + { + userName + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + httpsAgent, + } + ); + console.log('Throttling policy reset successfully.'); + res.status(200).json({ message: response.data.message }); + } catch (error) { + console.error('Error reseting throttling policy:', error.message); + res.status(500).json({ error: 'Failed to reset the throttling policy' }); + } +}; + +module.exports = { + loadApplications, + loadThrottlingPolicies, + loadApplication, + loadApplicationForEdit, + saveApplication, + updateApplication, + deleteApplication, + resetThrottlingPolicy +}; \ No newline at end of file diff --git a/src/controllers/customContentController.js b/src/controllers/customContentController.js index e535956..0f4a95c 100644 --- a/src/controllers/customContentController.js +++ b/src/controllers/customContentController.js @@ -42,7 +42,8 @@ const loadCustomContent = async (req, res) => { templateContent[tempKey] = loadMarkdown(filename, filePrefix + 'pages/' + filePath + '/content') }); } - html = renderTemplate(filePrefix + 'pages/' + filePath + '/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + html = renderTemplate(filePrefix + 'pages/' + filePath + '/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false) + } else { let content = {}; try { diff --git a/src/controllers/orgContentController.js b/src/controllers/orgContentController.js index 46ac325..223060a 100644 --- a/src/controllers/orgContentController.js +++ b/src/controllers/orgContentController.js @@ -45,7 +45,7 @@ const loadOrgContentFromFile = async () => { userProfiles: mockProfileData, baseUrl: constants.BASE_URL + config.port }; - return renderTemplate(filePrefix + 'pages/home/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + return renderTemplate(filePrefix + 'pages/home/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false) } const loadOrgContentFromAPI = async (req) => { @@ -60,8 +60,9 @@ const loadOrgContentFromAPI = async (req) => { console.log(`Rendering default organization landing page from file`); let templateContent = { baseUrl: '/' + orgName - }; - html = await renderTemplate(filePrefix + 'pages/home/page.hbs', filePrefix + 'layout/main.hbs', templateContent); + } + html = await renderTemplate(filePrefix + 'pages/home/page.hbs', filePrefix + 'layout/main.hbs', templateContent, false) + } return html; } diff --git a/src/middlewares/registerPartials.js b/src/middlewares/registerPartials.js index 971aafa..8ef5239 100644 --- a/src/middlewares/registerPartials.js +++ b/src/middlewares/registerPartials.js @@ -50,6 +50,24 @@ const registerAllPartialsFromFile = async (baseURL, req) => { registerPartialsFromFile(baseURL, path.join(process.cwd(), filePrefix, "pages", "home", "partials"), req.user); registerPartialsFromFile(baseURL, path.join(process.cwd(), filePrefix, "pages", "api-landing", "partials"), req.user); registerPartialsFromFile(baseURL, path.join(process.cwd(), filePrefix, "pages", "apis", "partials"), req.user); + + const applicationPartialPaths = [ + "applications", + "add-application", + "application", + "edit-application", + ]; + + applicationPartialPaths.forEach((page) => { + registerPartialsFromFile( + baseURL, + path.join(require.main.filename, "..", "pages", page, "partials"), + req.user + ); + }); + + registerPartialsFromFile(baseURL, path.join(require.main.filename, "..", "utils", "partials"), req.user); + if (fs.existsSync(path.join(process.cwd(), filePrefix + "pages", filePath, "partials"))) { registerPartialsFromFile(baseURL, path.join(process.cwd(), filePrefix + "pages", filePath, "partials"), req.user); } diff --git a/src/pages/add-application/page.hbs b/src/pages/add-application/page.hbs new file mode 100644 index 0000000..4cd14c5 --- /dev/null +++ b/src/pages/add-application/page.hbs @@ -0,0 +1,4 @@ +
+ {{> add-application-form }} + {{> alert }} +
\ No newline at end of file diff --git a/src/pages/add-application/partials/add-application-form.hbs b/src/pages/add-application/partials/add-application-form.hbs new file mode 100644 index 0000000..f6cc828 --- /dev/null +++ b/src/pages/add-application/partials/add-application-form.hbs @@ -0,0 +1,97 @@ + + + + + + +
+
+
+

Create an Application

+

+ Create an application providing name and quota parameters. Description + is optional. Required fields are marked with an asterisk (*). +

+
+ +
+ + +
Enter a name to identify the application. You + will be able to pick this application when subscribing to APIs.
+ +
+ + +
+ + +
Assign API request quota per access token. + Allocated quota will be shared among all the subscribed APIs of + the application.
+
+ + +
+ + +
+ 512 + characters remaining +
+ +
+ + +
+ + +
+
+
+
+
+ diff --git a/src/pages/application/page.hbs b/src/pages/application/page.hbs new file mode 100644 index 0000000..abd35e8 --- /dev/null +++ b/src/pages/application/page.hbs @@ -0,0 +1,6 @@ +
+ {{> application-dashboard }} + {{> delete-confirmation }} + {{> alert }} + {{> throttling-reset-modal}} +
\ No newline at end of file diff --git a/src/pages/application/partials/application-dashboard.hbs b/src/pages/application/partials/application-dashboard.hbs new file mode 100644 index 0000000..9e59b51 --- /dev/null +++ b/src/pages/application/partials/application-dashboard.hbs @@ -0,0 +1,90 @@ + + + + + + +
+
+
+ + +
+
+ {{> overview }} +
+
+ {{> prod-oauth2 }} +
+
+ {{> prod-apikey}} +
+
+ {{> sandbox-oauth2 }} +
+
+ {{> sandbox-apikey }} +
+
+ {{> subscriptions }} +
+
+
+
+
+ \ No newline at end of file diff --git a/src/pages/application/partials/overview.hbs b/src/pages/application/partials/overview.hbs new file mode 100644 index 0000000..eb02c28 --- /dev/null +++ b/src/pages/application/partials/overview.hbs @@ -0,0 +1,36 @@ + + + + +
+ {{#applicationMetadata}} +
+

{{name}}

+
+ +
+
+

Subscriptions: {{subscriptionCount}}

+
+
+

Details

+

Description: {{description}}

+

Business Plan: {{throttlingPolicy}}

+
+ +
+
+
+

Status

+

Workflow Status: + {{status}}

+

Application Owner: {{owner}}

+
+
+ {{/applicationMetadata}} +
\ No newline at end of file diff --git a/src/pages/application/partials/prod-apikey.hbs b/src/pages/application/partials/prod-apikey.hbs new file mode 100644 index 0000000..bd8c348 --- /dev/null +++ b/src/pages/application/partials/prod-apikey.hbs @@ -0,0 +1 @@ +

Hello prod-apikey

\ No newline at end of file diff --git a/src/pages/application/partials/prod-oauth2.hbs b/src/pages/application/partials/prod-oauth2.hbs new file mode 100644 index 0000000..049f4e9 --- /dev/null +++ b/src/pages/application/partials/prod-oauth2.hbs @@ -0,0 +1 @@ +

Hello prod-oauth2

\ No newline at end of file diff --git a/src/pages/application/partials/sandbox-apikey.hbs b/src/pages/application/partials/sandbox-apikey.hbs new file mode 100644 index 0000000..5f778d3 --- /dev/null +++ b/src/pages/application/partials/sandbox-apikey.hbs @@ -0,0 +1 @@ +

Hello sandbox-apikey

\ No newline at end of file diff --git a/src/pages/application/partials/sandbox-oauth2.hbs b/src/pages/application/partials/sandbox-oauth2.hbs new file mode 100644 index 0000000..1503cd6 --- /dev/null +++ b/src/pages/application/partials/sandbox-oauth2.hbs @@ -0,0 +1 @@ +

Hello sandbox-oauth2

\ No newline at end of file diff --git a/src/pages/application/partials/subscriptions.hbs b/src/pages/application/partials/subscriptions.hbs new file mode 100644 index 0000000..56a69bb --- /dev/null +++ b/src/pages/application/partials/subscriptions.hbs @@ -0,0 +1 @@ +

Hello subscriptions

\ No newline at end of file diff --git a/src/pages/application/partials/throttling-reset-modal.hbs b/src/pages/application/partials/throttling-reset-modal.hbs new file mode 100644 index 0000000..8b9888a --- /dev/null +++ b/src/pages/application/partials/throttling-reset-modal.hbs @@ -0,0 +1,68 @@ + + + + + + +
+ + +
+ \ No newline at end of file diff --git a/src/pages/applications/page.hbs b/src/pages/applications/page.hbs new file mode 100644 index 0000000..ff6cb34 --- /dev/null +++ b/src/pages/applications/page.hbs @@ -0,0 +1,6 @@ +
+ {{> applications-listing }} + {{> delete-confirmation }} + {{> community }} + {{> alert }} +
\ No newline at end of file diff --git a/src/pages/applications/partials/applications-listing.hbs b/src/pages/applications/partials/applications-listing.hbs new file mode 100644 index 0000000..262d584 --- /dev/null +++ b/src/pages/applications/partials/applications-listing.hbs @@ -0,0 +1,118 @@ + + + + + +
+
+
+
+
+
+

Subscribe to + APIs + by creating applications. +

+
+
+

Create, manage, or remove applications here.

+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ {{#applicationsMetadata}} +
+
+
+ {{name}} +
+
+
+ {{status}} + Owner: + {{owner}} +
+
+
+

+ + Subscriptions: + {{subscriptionCount}} +

+

+ + Throttling Policy: + {{throttlingPolicy}} +

+
+
+ + +
+
+
+ {{/applicationsMetadata}} +
+
+
+
+
+
+
+
diff --git a/src/pages/edit-application/page.hbs b/src/pages/edit-application/page.hbs new file mode 100644 index 0000000..d0014b9 --- /dev/null +++ b/src/pages/edit-application/page.hbs @@ -0,0 +1,4 @@ +
+ {{> edit-application-form }} + {{> alert }} +
\ No newline at end of file diff --git a/src/pages/edit-application/partials/edit-application-form.hbs b/src/pages/edit-application/partials/edit-application-form.hbs new file mode 100644 index 0000000..114d892 --- /dev/null +++ b/src/pages/edit-application/partials/edit-application-form.hbs @@ -0,0 +1,74 @@ + + + + + + +
+
+
+

Edit Application

+

+ Edit this application. Name and quota are mandatory parameters and description + is optional. Required fields are marked with an asterisk (*). +

+ {{#applicationMetadata}} +
+ +
+ + +
Enter a name to identify the application. You will be able to pick this application when subscribing to APIs.
+ +
+ + +
+ + +
Assign API request quota per access token. Allocated quota will be shared among all the subscribed APIs of the application.
+
+ + +
+ + +
+ 512 characters remaining +
+ +
+ + +
+ + +
+
+ {{/applicationMetadata}} +
+
+
+ diff --git a/src/routes/applicationsContentRoute.js b/src/routes/applicationsContentRoute.js new file mode 100644 index 0000000..5e5c2d2 --- /dev/null +++ b/src/routes/applicationsContentRoute.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const applicationsController = require('../controllers/applicationsContentController'); +const registerPartials = require('../middlewares/registerPartials'); + +router.get('/((?!favicon.ico)):orgName/applications', registerPartials, applicationsController.loadApplications); +router.get('/((?!favicon.ico)):orgName/applications/create', registerPartials, applicationsController.loadThrottlingPolicies); +router.get('/((?!favicon.ico)):orgName/applications/:applicationid', registerPartials, applicationsController.loadApplication); +router.get('/((?!favicon.ico)):orgName/applications/:applicationid/edit', registerPartials, applicationsController.loadApplicationForEdit); + +router.post('/applications', applicationsController.saveApplication); +router.put('/applications/:applicationid', applicationsController.updateApplication); +router.delete('/applications/:applicationid', applicationsController.deleteApplication); +router.post('/applications/:applicationid/reset-throttle-policy', applicationsController.resetThrottlingPolicy); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/designModeRoute.js b/src/routes/designModeRoute.js index 3cd9d35..9504a9f 100644 --- a/src/routes/designModeRoute.js +++ b/src/routes/designModeRoute.js @@ -20,6 +20,7 @@ const router = express.Router(); const orgController = require('../controllers/orgContentController'); const apiController = require('../controllers/apiContentController'); const contentController = require('../controllers/customContentController'); +const applicationController = require('../controllers/applicationsContentController'); const registerPartials = require('../middlewares/registerPartials'); const authController = require('../controllers/authController'); @@ -32,6 +33,11 @@ router.get('/api/:apiName', registerPartials, apiController.loadAPIContent); router.get('/api/:apiName/tryout', registerPartials, apiController.loadTryOutPage); +router.get('/applications', registerPartials, applicationController.loadApplications); +router.get('/applications/create', registerPartials, applicationController.loadThrottlingPolicies); +router.get('/applications/:applicationid', registerPartials, applicationController.loadApplication); +router.get('/applications/:applicationid/edit', registerPartials, applicationController.loadApplicationForEdit); + router.get('/login', registerPartials, authController.login); router.get('/callback', registerPartials, authController.handleCallback); router.get('/logout', registerPartials, authController.handleLogOut); diff --git a/src/scripts/add-application-form.js b/src/scripts/add-application-form.js new file mode 100644 index 0000000..598c42b --- /dev/null +++ b/src/scripts/add-application-form.js @@ -0,0 +1,100 @@ +// Validation of the form + +document.addEventListener('DOMContentLoaded', () => { + const applicationNameInput = document.getElementById('applicationName'); + const descriptionTextarea = document.getElementById('applicationDescription'); + const remainingCharactersSpan = document.getElementById( + 'remainingCharacters' + ); + const nameError = document.getElementById('nameError'); + const descriptionError = document.getElementById('descriptionError'); + const saveButton = document.getElementById('saveButton'); + const cancelButton = document.getElementById('cancelButton'); + + const MAX_CHARACTERS = 512; + + const validateForm = () => { + let hasError = false; + + if (!applicationNameInput.value.trim()) { + nameError.style.display = 'block'; + hasError = true; + } else { + nameError.style.display = 'none'; + } + + const remaining = MAX_CHARACTERS - descriptionTextarea.value.length; + if (remaining < 0) { + descriptionError.style.display = 'block'; + hasError = true; + } else { + descriptionError.style.display = 'none'; + } + + saveButton.disabled = hasError; + }; + + descriptionTextarea.addEventListener('input', () => { + const remaining = Math.max( + 0, + MAX_CHARACTERS - descriptionTextarea.value.length + ); + remainingCharactersSpan.textContent = remaining; + validateForm(); + }); + + applicationNameInput.addEventListener('input', validateForm); + + cancelButton.addEventListener('click', () => { + window.history.back(); + }); + + document + .getElementById('applicationForm') + .addEventListener('submit', (event) => { + validateForm(); + if (saveButton.disabled) { + event.preventDefault(); + } + }); +}); + +// Submittion of the form + +const applicationForm = document.getElementById('applicationForm'); + +applicationForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const name = document.getElementById('applicationName').value; + const throttlingPolicy = document.getElementById('throttlingPolicy').value; + const description = document.getElementById( + 'applicationDescription' + ).value; + + try { + const response = await fetch('/applications', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + throttlingPolicy, + description, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const responseData = await response.json(); + await showAlert(responseData.message || 'Application saved successfully!', 'success'); + applicationForm.reset(); + window.location.href = document.referrer || '/applications'; + } catch (error) { + console.error('Error saving application:', error); + await showAlert('Failed to save application.', 'error'); + } +}); diff --git a/src/scripts/alert.js b/src/scripts/alert.js new file mode 100644 index 0000000..53c8e55 --- /dev/null +++ b/src/scripts/alert.js @@ -0,0 +1,24 @@ +function showAlert(message, type) { + return new Promise((resolve) => { + const modalElement = document.getElementById('alertModal'); + const modalMessage = modalElement.querySelector('.modal-message'); + const modalBody = modalElement.querySelector('.modal-body'); + + modalMessage.textContent = message; + + modalBody.classList.remove('success', 'error'); + modalBody.classList.add(type); + + const bootstrapModal = new bootstrap.Modal(modalElement, { backdrop: false }); + bootstrapModal.show(); + + setTimeout(() => { + modalElement.classList.add('fade-out'); + setTimeout(() => { + bootstrapModal.hide(); + modalElement.classList.remove('fade-out'); + resolve(); + }, 500); + }, 2300); + }); +} diff --git a/src/scripts/application-dashboard.js b/src/scripts/application-dashboard.js new file mode 100644 index 0000000..56d64f1 --- /dev/null +++ b/src/scripts/application-dashboard.js @@ -0,0 +1,91 @@ +// ***** Dashboard Naivgation ***** + +document.addEventListener('DOMContentLoaded', function () { + const navLinks = document.querySelectorAll('.nav-link[data-section]'); + const sections = document.querySelectorAll('.content-section'); + + navLinks.forEach((link) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + const targetSectionId = link.getAttribute('data-section'); + + // Hide all sections + sections.forEach((section) => section.classList.add('d-none')); + + // Show the target section + const targetSection = document.getElementById(targetSectionId); + if (targetSection) { + targetSection.classList.remove('d-none'); + } + }); + }); +}); + +// ***** Throttling Policy Reset Modal ***** + +// Open the Throttling Policy Reset Modal + +function openResetModal() { + const modal = document.getElementById('throttlingPolicyResetModal'); + const bootstrapModal = new bootstrap.Modal(modal); + bootstrapModal.show(); +} + +// Validation of the Reset Throttling Policy Form + +document.addEventListener('DOMContentLoaded', () => { + const userName = document.getElementById('userName'); + const userNameError = document.getElementById('userNameError'); + const resetButton = document.getElementById('resetButton'); + + const validateForm = () => { + let hasError = false; + if (!userName.value.trim()) { + userNameError.style.display = 'block'; + hasError = true; + } else { + userNameError.style.display = 'none'; + } + + resetButton.disabled = hasError; + }; + userName.addEventListener('input', validateForm); +}); + +// Reset Throttling Policy Submit + +async function resetThrottlingPolicy(applicationId) { + const userName = document.getElementById('userName').value; + + console.log('Resetting throttling policy...'); + try { + const response = await fetch( + `/applications/${applicationId}/reset-throttle-policy`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userName, + }), + } + ); + if (response.ok) { + console.log('Throttling policy reset successfully.'); + await showAlert('Throttling policy reset successfully!', 'success'); + } else { + console.error('Failed to reset throttling policy.'); + await showAlert( + 'Failed to reset throttling policy. Please try again.', + 'error' + ); + } + } catch (error) { + console.error('Error resetting throttling policy:', error); + await showAlert( + 'An error occurred while resetting the throttling policy. Please try again.', + 'error' + ); + } +} diff --git a/src/scripts/application-listing.js b/src/scripts/application-listing.js new file mode 100644 index 0000000..b33bf56 --- /dev/null +++ b/src/scripts/application-listing.js @@ -0,0 +1,27 @@ +// Search functionality for the application listing page + +document.addEventListener('DOMContentLoaded', () => { + const queryInput = document.getElementById('query'); + const applicationsContainer = document.getElementById( + 'applicationCardsContainer' + ); + const allCards = Array.from(applicationsContainer.children); + queryInput.addEventListener('input', () => { + const query = queryInput.value.trim().toLowerCase(); + if (!query) { + applicationsContainer.innerHTML = ''; + allCards.forEach((card) => { + applicationsContainer.appendChild(card); + }); + return; + } + const filteredCards = allCards.filter((card) => { + const appName = card.getAttribute('data-name').toLowerCase(); + return appName.includes(query); + }); + applicationsContainer.innerHTML = ''; + filteredCards.forEach((card) => { + applicationsContainer.appendChild(card); + }); + }); +}); diff --git a/src/scripts/delete-confirmation.js b/src/scripts/delete-confirmation.js new file mode 100644 index 0000000..3891dd3 --- /dev/null +++ b/src/scripts/delete-confirmation.js @@ -0,0 +1,25 @@ +function openDeleteModal(applicationId) { + const modal = document.getElementById('deleteConfirmation'); + modal.dataset.applicationId = applicationId; + const bootstrapModal = new bootstrap.Modal(modal); + bootstrapModal.show(); +} + +async function deleteApplication() { + const modal = document.getElementById('deleteConfirmation'); + const applicationId = modal.dataset.applicationId; + try { + const response = await fetch(`/applications/${applicationId}`, { method: 'DELETE' }); + if (response.ok) { + console.log('Application deleted successfully.'); + await showAlert('Application deleted successfully!', 'success'); + } else { + console.error('Failed to delete application.'); + await showAlert('Failed to delete application. Please try again.', 'error'); + } + } catch (error) { + console.error('Error deleting application:', error); + await showAlert('An error occurred while deleting the application. Please try again.', 'error'); + } + window.location.reload(true); +} diff --git a/src/scripts/edit-application-form.js b/src/scripts/edit-application-form.js new file mode 100644 index 0000000..f3d9ad7 --- /dev/null +++ b/src/scripts/edit-application-form.js @@ -0,0 +1,99 @@ +// Validation of the form + +document.addEventListener('DOMContentLoaded', () => { + const applicationNameInput = document.getElementById('editApplicationName'); + const descriptionTextarea = document.getElementById( + 'editApplicationDescription' + ); + const remainingCharactersSpan = document.getElementById( + 'editApplicationRemainingCharacters' + ); + const nameError = document.getElementById('editApplicationNameError'); + const descriptionError = document.getElementById( + 'editApplicationDescriptionError' + ); + const editButton = document.getElementById('editApplicationEditButton'); + const cancelButton = document.getElementById('editApplicationCancelButton'); + + const MAX_CHARACTERS = 512; + + const validateForm = () => { + let hasError = false; + if (!applicationNameInput.value.trim()) { + nameError.style.display = 'block'; + hasError = true; + } else { + nameError.style.display = 'none'; + } + const remaining = MAX_CHARACTERS - descriptionTextarea.value.length; + if (remaining < 0) { + descriptionError.style.display = 'block'; + hasError = true; + } else { + descriptionError.style.display = 'none'; + } + editButton.disabled = hasError; + }; + descriptionTextarea.addEventListener('input', () => { + const remaining = Math.max( + 0, + MAX_CHARACTERS - descriptionTextarea.value.length + ); + remainingCharactersSpan.textContent = remaining; + validateForm(); + }); + applicationNameInput.addEventListener('input', validateForm); + cancelButton.addEventListener('click', () => { + window.history.back(); + }); + document + .getElementById('editApplicationForm') + .addEventListener('submit', (event) => { + validateForm(); + if (editButton.disabled) { + event.preventDefault(); + } + }); +}); + +// Submittion of the form + +const form = document.getElementById('editApplicationForm'); +const applicationId = form.dataset.applicationId; + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + const name = document.getElementById('editApplicationName').value; + const throttlingPolicy = document.getElementById( + 'editApplicationThrottlingPolicy' + ).value; + const description = document.getElementById( + 'editApplicationDescription' + ).value; + try { + const response = await fetch(`/applications/${applicationId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + throttlingPolicy, + description, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const responseData = await response.json(); + await showAlert( + responseData.message || 'Application updated successfully!', + 'success' + ); + form.reset(); + window.location.href = document.referrer || '/applications'; + } catch (error) { + console.error('Error saving application:', error); + await showAlert('Failed to update application.', 'error'); + } +}); diff --git a/src/styles/add-edit-application.css b/src/styles/add-edit-application.css new file mode 100644 index 0000000..648e97b --- /dev/null +++ b/src/styles/add-edit-application.css @@ -0,0 +1,47 @@ +.form-card { + display: flex; + flex-direction: column; + border: 1px solid var(--border-colour-secondary); + border-radius: 12px; + background-color: var(--light-bg-color); + padding: 20px; + height: 100%; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.title { + font-size: 24px; + margin: 0; + color: var(--primary-color); +} + +.button i { + margin-right: 5px; +} + +.form-label { + font-size: 14px; + color: var(--dark-text-color); + margin: 5px 0; +} + +.form-control, +.form-select { + font-size: 14px; + color: var(--light-text-color); +} + +.custom-cancel-btn, +.custom-save-btn { + color: var(--font-colour-primary); + border-radius: 20px; + padding: 10px 20px; + border: 0px; + font-size: 14px; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; + text-decoration: none; +} + +.error-msg { + font-size: 14px; +} \ No newline at end of file diff --git a/src/styles/alert.css b/src/styles/alert.css new file mode 100644 index 0000000..9c25f23 --- /dev/null +++ b/src/styles/alert.css @@ -0,0 +1,44 @@ +.custom-alert .modal-dialog { + position: fixed; + bottom: 20px; + right: 20px; + width: auto; + max-width: 500px; + animation: fadeIn 0.5s; + z-index: 1055; +} + +.custom-alert .modal-content { + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + padding: 0; +} + +.custom-alert .modal-body { + font-size: 14px; + height: 100%; + display: flex; + align-items: center; +} + +.custom-alert .modal-body.success { + background-color: #95d5b2; + color: #ffffff; +} + +.custom-alert .modal-body.error { + background-color: #ff8585; + color: #ffffff; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/styles/application.css b/src/styles/application.css new file mode 100644 index 0000000..3a52f4d --- /dev/null +++ b/src/styles/application.css @@ -0,0 +1,49 @@ +.sidebar { + background-color: var(--main-bg-color); + height: 100vh; + padding: 1rem 0; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border-color); +} + +.sidebar .nav-link { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + color: var(--light-text-color); + font-size: 1rem; + text-decoration: none; + transition: all 0.3s ease; +} + +.sidebar .nav-link:hover { + background-color: var(--border-color); + color: var(--primary-color); + border-radius: 5px; +} + +.sidebar .nav-link i { + font-size: 1rem; + margin-right: 0.5rem; +} + +.sidebar .menu-text { + margin-top: 0.5rem; + font-size: 1rem; + margin-right: 1rem; +} + +.sidebar .sub-menu { + margin-left: 1rem; + padding-left: 1rem; + border-left: 2px solid var(--border-color); +} + +.sidebar .sub-menu .nav-link { + font-size: 0.95rem; +} + +.content { + padding: 2rem; +} diff --git a/src/styles/applications.css b/src/styles/applications.css new file mode 100644 index 0000000..0187376 --- /dev/null +++ b/src/styles/applications.css @@ -0,0 +1,231 @@ +body { + background-color: var(--main-bg-color); +} + +.hero-section-applicationlisting { + display: flex; + justify-content: center; + align-items: center; + padding: 10px 0; + background-color: var(--primary-color); + height: auto; +} + +.applicationlist-hero-container { + width: 100%; + max-width: 1000px; + text-align: center; + margin-top: 30px; +} + +.applicationlist-hero-content h1, +.applicationlist-hero-content p, +.applicationlist-hero-content span { + color: var(--font-colour-primary); +} + +.applicationlist-hero-content h1 { + font-size: 35px; +} + +.applicationlist-hero-content p { + font-size: 14px; + line-height: 24px; +} + +.applicationlist-hero-header { + max-width: 800px; +} +.applicationlist-hero-content span { + font-size: 35px; + margin-bottom: 20px; + color: var(--light-color); +} + +.applicationlist-hero-content { + margin: 40px 0; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.search-container { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.search-input { + display: flex; + align-items: center; + background-color: transparent; + border: 1px solid var(--border-colour-secondary); + padding: 10px 15px; + border-radius: 25px; + width: 100%; + height: 40px; +} + +.search-input input { + border: none; + outline: none; + width: 100%; + margin-left: 10px; + font-size: 14px; + background-color: transparent; + color: var(--main-bg-color); +} + +#query::placeholder { + color: var(--main-bg-color); + opacity: 0.6; +} + +.bi-search { + color: var(--main-bg-color); + font-size: 20px; + position: relative; + top: -2px; +} + +.applicationspage-container { + padding: 10px 65px; + margin-top: 45px; + margin-bottom: 20px; +} + +.applicationlist-container { + display: flex; + flex-wrap: wrap; + justify-content: left; +} + +.application-card { + display: flex; + flex-direction: column; + border: 1px solid var(--border-colour-secondary); + border-radius: 12px; + background-color: var(--light-bg-color); + padding: 20px; + height: 100%; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.application-card:hover { + transform: translateY(-5px); + box-shadow: 0px 6px 15px rgba(0, 0, 0, 0.2); +} + +.application-card-body { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + +.application-card-title { + font-size: 18px; + color: var(--main-text-color); + margin-bottom: 10px; +} + +.application-name-link { + color: var(--primary-color); + text-decoration: none; +} + +.application-name-link:hover { + color: var(--warning-color); +} + +.application-card-description { + font-size: 14px; + color: var(--secondary-text-color); + margin-bottom: 15px; +} + +.application-card-meta p { + display: flex; + align-items: center; + font-size: 12px; + color: var(--secondary-text-color); + margin: 5px 0; +} + +.application-card-meta i { + margin-right: 8px; + color: var(--primary-color); +} + +.badge-custom1 { + padding: 5px 10px; + border-radius: 12px; + font-size: 12px; +} + +.application-card-actions { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background-color: transparent; + color: var(--primary-color); + font-size: 18px; + cursor: pointer; +} + +.btn-icon:hover { + color: var(--warning-color); +} + +.applicationsbtn-container { + margin: 20px; + width: 100%; +} + +.applicationsaddbtn-primary { + padding: 5px 30px; + border-radius: 20px; + font-size: 14px; + background-color: transparent; + color: var(--border-colour-primary); + border: 1px solid var(--border-colour-primary); +} + +.applicationsaddbtn-primary:hover { + background-color: var(--primary-color); + color: var(--font-colour-primary); +} + +@media (max-width: 1200px) { + .col-lg-2 { + flex: 0 0 33.3333%; + max-width: 33.3333%; + } +} + +@media (max-width: 768px) { + .col-lg-2, + .col-md-4 { + flex: 0 0 50%; + max-width: 50%; + } +} + +@media (max-width: 576px) { + .col-lg-2, + .col-md-4, + .col-sm-6 { + flex: 0 0 100%; + max-width: 100%; + } +} diff --git a/src/styles/delete-confirmation.css b/src/styles/delete-confirmation.css new file mode 100644 index 0000000..94481c7 --- /dev/null +++ b/src/styles/delete-confirmation.css @@ -0,0 +1,23 @@ +.delete-alert .modal-dialog { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: auto; + animation: fadeIn 1s; + z-index: 1055; +} + +.delete-alert .modal-content { + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + padding: 0; +} + +.delete-alert .modal-body { + font-size: 14px; + height: 100%; + display: flex; + align-items: center; +} diff --git a/src/styles/overview.css b/src/styles/overview.css new file mode 100644 index 0000000..1d6db96 --- /dev/null +++ b/src/styles/overview.css @@ -0,0 +1,80 @@ +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #ddd; + padding-bottom: 10px; +} + +.app-title { + font-size: 24px; + margin: 0; + color: var(--primary-color); +} + +.action-buttons { + display: flex; + gap: 10px; +} + +.app-details { + margin-top: 20px; +} + +.details-section, +.status-section { + margin-bottom: 20px; +} + +h2 { + font-size: 18px; + color: var(--primary-color); + margin-bottom: 10px; +} + +p { + font-size: 14px; + color: var(--light-text-color); + margin: 5px 0; +} + +p2 { + font-size: 10px; +} + +.business-plan-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.reset-btn, +.info-btn { + padding: 5px 10px; + font-size: 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + background-color: var(--font-colour-primary); + color: var(--light-text-color); + display: flex; + align-items: center; + gap: 5px; +} + +.reset-btn:hover, +.info-btn:hover { + background-color: var(--light-color); +} + + +@media (max-width: 600px) { + .app-header { + flex-direction: column; + align-items: flex-start; + } + + .action-buttons { + margin-top: 10px; + } +} diff --git a/src/styles/throttling-reset-modal.css b/src/styles/throttling-reset-modal.css new file mode 100644 index 0000000..dc03c53 --- /dev/null +++ b/src/styles/throttling-reset-modal.css @@ -0,0 +1,23 @@ +.throttling-reset-alert .modal-dialog { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: auto; + animation: fadeIn 1s; + z-index: 1055; +} + +.throttling-reset-alert .modal-content { + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + padding: 0; +} + +.throttling-reset-alert .modal-body { + font-size: 14px; + height: 100%; + display: flex; + align-items: center; +} diff --git a/src/utils/constants.js b/src/utils/constants.js index e2ab771..bfea4a4 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -67,6 +67,8 @@ module.exports = { ROUTE: { DEV_PORTAL: '/devportal', STYLES: '/styles', + TECHNICAL_STYLES: '/technical-styles', + TECHNICAL_SCRIPTS: '/technical-scripts', IMAGES: '/images', IMAGES_PATH: '/images/', DEFAULT: '/', diff --git a/src/utils/partials/alert.hbs b/src/utils/partials/alert.hbs new file mode 100644 index 0000000..1903ff6 --- /dev/null +++ b/src/utils/partials/alert.hbs @@ -0,0 +1,23 @@ + + + + + + +
+ +
+ diff --git a/src/utils/partials/delete-confirmation.hbs b/src/utils/partials/delete-confirmation.hbs new file mode 100644 index 0000000..185cbca --- /dev/null +++ b/src/utils/partials/delete-confirmation.hbs @@ -0,0 +1,43 @@ + + + + + + +
+ +
+ \ No newline at end of file diff --git a/src/utils/util.js b/src/utils/util.js index d780c5b..1ae11a5 100644 --- a/src/utils/util.js +++ b/src/utils/util.js @@ -40,10 +40,10 @@ function loadMarkdown(filename, dirName) { }; -function renderTemplate(templatePath, layoutPath, templateContent) { - +function renderTemplate(templatePath, layoutPath, templateContent, isTechnical) { + let completeTemplatePath; - if (templatePath.includes('tryout')) { + if (isTechnical) { completeTemplatePath = path.join(require.main.filename, templatePath); } else { completeTemplatePath = path.join(process.cwd(), templatePath);