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 @@
+
+
+
+
+
+
+
+
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}}
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reset the Application Throttle Policy for a
+ Specific User
+
+
User Name
+ *
+
+
Enter the user name you Specificaly need to
+ reset the throttle policy for.
+
User Name cannot be empty.
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Create, manage, or remove applications here.
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#applicationsMetadata}}
+
+
+
+
+
+ {{status}}
+ Owner:
+ {{owner}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/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 @@
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
This Application will be removed
+
+
+
+
+
+
+
\ 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);