diff --git a/404.html b/404.html new file mode 100644 index 0000000..de1a8b8 --- /dev/null +++ b/404.html @@ -0,0 +1,25 @@ + + + + + + + + + +

Page not found.

+ + + \ No newline at end of file diff --git a/css/styles.css b/css/styles.css index ce97cee..70a4bdb 100644 --- a/css/styles.css +++ b/css/styles.css @@ -6,27 +6,33 @@ textarea { margin-bottom: 10px; } -#auto-generation { +#auto-generation, +#exemptions { border-radius: 15px; padding: 20px; padding-bottom: 25px; margin-top: 20px; margin-bottom: 20px; background: lightgray; - width: 100%; - max-width: 800px; - box-sizing: border-box; + width: 100%; + max-width: 800px; + box-sizing: border-box; } -#github-url-form { +#exemptions { + max-width: 1300px; +} + +#github-url-form, +#exemptions-checklist { margin-top: 25px; width: 100%; } #repo-url { - width: 100%; - max-width: 750px; - box-sizing: border-box; + width: 100%; + max-width: 750px; + box-sizing: border-box; } #repo-url-button { @@ -50,12 +56,33 @@ textarea { padding-right: 20px; } +.step-header { + display: flex; + align-items: center; + gap: 0.5em; +} + +.step-number { + margin-top: 10px; + background-color: #005ea2; + color: white; + font-weight: bold; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + @keyframes slideDown { - from { + from { opacity: 0; transform: translateY(-10px); } - to { + + to { opacity: 1; transform: translateY(0); } diff --git a/index.html b/index.html index 80ca1d2..8b65d8c 100644 --- a/index.html +++ b/index.html @@ -1,85 +1,191 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + - - - - - + + + + + + + - - -
+ }) + .catch((error) => { + console.error("Error creating components:", error); + }); + + - - -
-

-
-
-
- -
-
+ +
+ +
+

+
+
+ +
Security & Privacy
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
National Security and Intelligence
+
+ + +
+
+ + +
+
+ + +
+
Export and Regulatory Compliance
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
-
+ -
- - - - - - +
+

+
+
+
+ +
+
+ +
+
+
3
+

Complete form and generate code.json file

+
+
+ +
+ +
+ + + + + + +
+ + - - + \ No newline at end of file diff --git a/js/autoGenerateFields.js b/js/autoGenerateFields.js index d018559..86bf652 100644 --- a/js/autoGenerateFields.js +++ b/js/autoGenerateFields.js @@ -208,7 +208,13 @@ async function preFillFields(repoData, languages) { // Updating URL if (repoData.html_url) { - form.getComponent('repositoryURL').setValue(repoData.html_url) + if (repoData.private) { + // Private repositories must have "private" as their repositoryURL value + form.getComponent('repositoryURL').setValue("private") + } + else { + form.getComponent('repositoryURL').setValue(repoData.html_url) + } } // Updating forks @@ -306,11 +312,11 @@ async function preFillFields(repoData, languages) { } // fields to potentially automate - // clones, but this is only tracked for every 14 days - // status, by checking if its public, we can assume its production and check if its archival - // laborHours, by running a script? this might be harder since we need SCC - // maturityModel, we could check to see if certain files / sections live within a repo and make a guess like that - // usageType, by assuming that if its public = openSource and if private = governmnetWideReuse + // clones, but this is only tracked for every 14 days + // status, by checking if its public, we can assume its production and check if its archival + // laborHours, by running a script? this might be harder since we need SCC + // maturityModel, we could check to see if certain files / sections live within a repo and make a guess like that + // usageType, by assuming that if its public = openSource and if private = governmnetWideReuse notificationSystem.success("Repository data loaded successfully!") diff --git a/js/formDataToJson.js b/js/formDataToJson.js index 9b8f0a0..c4d939a 100644 --- a/js/formDataToJson.js +++ b/js/formDataToJson.js @@ -16,23 +16,23 @@ async function retrieveFile(filePath) { function isMultiSelect(obj) { for (const key in obj) { - if (typeof obj[key] !== 'boolean') { - return false; - } + if (typeof obj[key] !== 'boolean') { + return false; + } } return true; // Returns true if all values are booleans } // Convert from dictionary to array function getSelectedOptions(options) { - let selectedOptions = []; - - for (let key in options) { - if(options[key]) { - selectedOptions.push(key); - } - } - return selectedOptions; + let selectedOptions = []; + + for (let key in options) { + if (options[key]) { + selectedOptions.push(key); + } + } + return selectedOptions; } // Populates fields with form data @@ -46,7 +46,7 @@ function populateObject(data, schema) { let value = data[key]; // Adjusts value accordingly if multi-select field - if((typeof value === "object" && isMultiSelect(value))) { + if ((typeof value === "object" && isMultiSelect(value))) { value = getSelectedOptions(value); } @@ -57,7 +57,10 @@ function populateObject(data, schema) { } async function populateCodeJson(data) { - const filePath = "schemas/schema.json"; + // Fetching schema based on search params + const params = new URLSearchParams(window.location.search); + const page = params.get("page") || "gov"; + const filePath = `schemas/${page}/schema.json`; // Retrieves schema with fields in correct order const schema = await retrieveFile(filePath); @@ -87,37 +90,33 @@ async function createCodeJson(data) { } // Copies code.json to clipboard -async function copyToClipboard(event){ +async function copyToClipboard(event) { event.preventDefault(); var textArea = document.getElementById("json-result"); - textArea.select(); + textArea.select(); document.execCommand("copy") } const NEW_BRANCH = 'code-json-branch' + Math.random().toString(36).substring(2, 10); -function getOrgAndRepoArgsGitHub(url) -{ +function getOrgAndRepoArgsGitHub(url) { const pattern = /https:\/\/github\.com\/([^\/]+)\/([^\/]+)/; - const match = url.match(pattern); + const match = url.match(pattern); - if(match) - { + if (match) { const owner = match[1]; const repo = match[2]; - return {owner,repo}; + return { owner, repo }; } - else - { + else { throw new Error('Invalid URL!'); } } -async function createBranchOnProject(projectURL, token) -{ - const {owner, repo} = getOrgAndRepoArgsGitHub(projectURL); +async function createBranchOnProject(projectURL, token) { + const { owner, repo } = getOrgAndRepoArgsGitHub(projectURL); const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, { @@ -130,41 +129,37 @@ async function createBranchOnProject(projectURL, token) const data = await response.json(); - if (response.ok) - { + if (response.ok) { const sha = data.object.sha; - + const createBranchApiUrl = `https://api.github.com/repos/${owner}/${repo}/git/refs`; // Create the new branch from the base branch const newBranchResponse = await fetch(createBranchApiUrl, { method: 'POST', headers: { - 'Content-Type': 'application/json', - 'Authorization': `token ${token}`, + 'Content-Type': 'application/json', + 'Authorization': `token ${token}`, }, body: JSON.stringify({ - ref: `refs/heads/${NEW_BRANCH}`, // Name of the new branch - sha: sha, // SHA of the base branch (main) + ref: `refs/heads/${NEW_BRANCH}`, // Name of the new branch + sha: sha, // SHA of the base branch (main) }), }); const newBranchData = await newBranchResponse.json(); - if ( newBranchResponse.ok ) - { + if (newBranchResponse.ok) { console.log('New branch created successfully: ', newBranchData); return true; } - else - { + else { console.error('Error creating new branch: ', newBranchData); alert("Failed to create branch on project! Error code: " + newBranchResponse.status + ". Please check API Key permissions and try again.") return false; } } - else - { + else { console.error('Error fetching base branch info:', data); alert('Error fetching base branch info:', data); return false; @@ -172,16 +167,15 @@ async function createBranchOnProject(projectURL, token) } -async function addFileToBranch(projectURL, token, codeJSONObj) -{ - const {owner, repo} = getOrgAndRepoArgsGitHub(projectURL); +async function addFileToBranch(projectURL, token, codeJSONObj) { + const { owner, repo } = getOrgAndRepoArgsGitHub(projectURL); const FILE_PATH = 'code.json' const createFileApiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${FILE_PATH}`; const encodedContent = btoa(codeJSONObj); console.log("Content: ", encodedContent); console.log("Branch: ", NEW_BRANCH); - const response = await fetch(createFileApiUrl, + const response = await fetch(createFileApiUrl, { method: 'PUT', headers: { @@ -203,24 +197,21 @@ async function addFileToBranch(projectURL, token, codeJSONObj) const data = await response.json() - if ( response.ok ) - { + if (response.ok) { console.log('File added successfully: ', data); return true; } - else - { + else { console.error('Error adding file: ', data); alert("Failed to add file on project! Error code: " + response.status + ". Please check API Key permissions and try again.") return false; } } -async function createPR(projectURL, token) -{ - const {owner, repo} = getOrgAndRepoArgsGitHub(projectURL); +async function createPR(projectURL, token) { + const { owner, repo } = getOrgAndRepoArgsGitHub(projectURL); const createPrApiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`; - const response = await fetch(createPrApiUrl, + const response = await fetch(createPrApiUrl, { method: 'POST', headers: { @@ -240,13 +231,11 @@ async function createPR(projectURL, token) const data = await response.json(); - if (response.ok) - { + if (response.ok) { console.log('Pull request created successfully: ', data); return true; } - else - { + else { console.error("Error creating PR!: ", data); alert("Failed to create PR on project! Error code: " + response.status + ". Please check API Key permissions and try again.") return false; @@ -254,50 +243,42 @@ async function createPR(projectURL, token) } // Creates PR on requested project -async function createProjectPR(event){ +async function createProjectPR(event) { event.preventDefault(); var textArea = document.getElementById("json-result");//Step 1 var codeJSONObj = JSON.parse(textArea.value) - - if('gh_api_key' in window) - { + + if ('gh_api_key' in window) { var apiKey = window.gh_api_key; - - if ('repositoryURL' in codeJSONObj) - { + + if ('repositoryURL' in codeJSONObj) { var prCreated = false; //Step 1 - const branchCreated = await createBranchOnProject(codeJSONObj.repositoryURL,apiKey); - if (branchCreated) - { + const branchCreated = await createBranchOnProject(codeJSONObj.repositoryURL, apiKey); + if (branchCreated) { const fileAdded = await addFileToBranch(codeJSONObj.repositoryURL, apiKey, textArea.value); - if (fileAdded) - { + if (fileAdded) { prCreated = await createPR(codeJSONObj.repositoryURL, apiKey); - if(prCreated) - { + if (prCreated) { console.log("PR successfully created!"); alert("PR has been created!"); } } } - else - { + else { console.error("Could not create branch on requested repository with the requested API key!"); alert("Could not create branch on requested repository with the requested API key!"); } } - else - { + else { console.error("No URL found!"); alert("No URL given for project! Please provide project URL in repositoryURL text box"); } - + } - else - { + else { console.error("No API key found!"); alert("No API Key in submitted data! Please provide an API key"); } @@ -328,27 +309,27 @@ async function emailFile(event) { const codeJson = document.getElementById("json-result").value const jsonObject = JSON.parse(codeJson); - - try { - const cleanData = {...jsonObject}; - delete cleanData.submit; - const jsonString = JSON.stringify(cleanData, null, 2); + try { + const cleanData = { ...jsonObject }; + delete cleanData.submit; + + const jsonString = JSON.stringify(cleanData, null, 2); - const subject = "Code.json generator Results"; - const body = `Hello,\n\nHere are the code.json results:\n\n${jsonString}\n\nThank you!`; + const subject = "Code.json generator Results"; + const body = `Hello,\n\nHere are the code.json results:\n\n${jsonString}\n\nThank you!`; - const recipients = ["opensource@cms.hhs.gov"]; + const recipients = ["opensource@cms.hhs.gov"]; - const mailtoLink = `mailto:${recipients}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + const mailtoLink = `mailto:${recipients}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - window.location.href = mailtoLink; + window.location.href = mailtoLink; - console.log("Email client opened"); - } catch { - console.error("Error preparing email:", error); - showNotificationModal("Error preparing email. Please try again or copy the data manually.", 'error'); - } + console.log("Email client opened"); + } catch { + console.error("Error preparing email:", error); + showNotificationModal("Error preparing email. Please try again or copy the data manually.", 'error'); + } } window.createCodeJson = createCodeJson; diff --git a/js/generateFormComponents.js b/js/generateFormComponents.js index f708615..2b08519 100644 --- a/js/generateFormComponents.js +++ b/js/generateFormComponents.js @@ -23,7 +23,7 @@ function transformArrayToOptions(arr) { } // Function that handles validation object needed for each form component -function determineValidation(fieldName, fieldObject, requiredArray){ +function determineValidation(fieldName, fieldObject, requiredArray) { return { "required": requiredArray.includes(fieldName) } @@ -207,7 +207,7 @@ function createComponent(fieldName, fieldObject, requiredArray) { description: fieldObject["description"], validate }; - case "container": + case "container": return { label: fieldName, hideLabel: false, @@ -238,7 +238,7 @@ function createComponent(fieldName, fieldObject, requiredArray) { input: true, components: [], validate - }; + }; default: break; } @@ -247,29 +247,38 @@ function createComponent(fieldName, fieldObject, requiredArray) { // Adds heading containing schema information function createFormHeading(title, description) { const container = document.getElementById('form-header'); - container.innerHTML = `

${title}

\n

${description}

`; + container.innerHTML = ` +

${title}

\n +

${description}

\n +

Complete the form below to create a code.json file for your project:

\n + `; +} + +function createExemptionsBox() { + const container = document.getElementById("exemptions-header") + container.innerHTML = `
1

Is my project exempted from the SHARE IT Act?

\n

Answer the series of questions below to determine if your project falls under the 4 exemption categories according to the SHARE IT Act.

` } function createAutoGenerationBox() { const container = document.getElementById("auto-generation-header") - container.innerHTML = `

Auto Generate Fields

\n

Please enter your repositories GitHub URL in order to automatically pre-fill some of the fields in this form!

\n
This currently only works on public repositories
` + container.innerHTML = `
2

Auto Generate Fields

\n

Please enter your repositories GitHub URL in order to automatically pre-fill some of the fields in this form!

\n
This currently only works on public repositories
` } // Iterates through each json field and creates component array for Form.io -function createAllComponents(schema, prefix = ""){ +function createAllComponents(schema, prefix = "") { let components = []; if (schema.type === "object" && schema.properties) { const items = schema.properties.hasOwnProperty("items") ? schema.properties.items : schema.properties; - + let requiredArray = []; if (schema.hasOwnProperty("required")) { requiredArray = schema.required; } - for (const [key, value] of Object.entries(items)) { - + for (const [key, value] of Object.entries(items)) { + console.log("key at play:", key); const fullKey = prefix ? `${prefix}.${key}` : key; @@ -277,27 +286,31 @@ function createAllComponents(schema, prefix = ""){ if (fieldComponent.type === "container") { fieldComponent.components = createAllComponents(value, fullKey); - } + } else if (fieldComponent.type === "datagrid") { fieldComponent.components = createAllComponents(value.items, fullKey); } components.push(fieldComponent); - } - } + } + } - return components; + return components; } // Creates complete form based on input json schema async function createFormComponents() { let components = []; - const filePath = "schemas/schema.json"; + // Fetching schema based on search params + const params = new URLSearchParams(window.location.search); + const page = params.get("page") || "gov"; + const filePath = `schemas/${page}/schema.json`; const jsonData = await retrieveFile(filePath); console.log("JSON Data:", jsonData); createFormHeading(jsonData["title"], jsonData["description"]); + createExemptionsBox() createAutoGenerationBox() components = createAllComponents(jsonData); @@ -326,7 +339,7 @@ async function createFormComponents() { tableView: false, }); - + console.log(components); diff --git a/schemas/schema.json b/schemas/cms/schema.json similarity index 95% rename from schemas/schema.json rename to schemas/cms/schema.json index 989175a..239abe2 100644 --- a/schemas/schema.json +++ b/schemas/cms/schema.json @@ -23,13 +23,13 @@ "status": { "type": "string", "enum": [ - "Ideation", - "Development", - "Alpha", - "Beta", - "Release Candidate", - "Production", - "Archival" + "ideation", + "development", + "alpha", + "beta", + "releaseCandidate", + "production", + "archival" ], "description": "Development status of the project" }, @@ -114,7 +114,7 @@ "repositoryURL": { "type": "string", "format": "uri", - "description": "The URL of the public release repository for open source repositories. This field is not required for repositories that are only available as government-wide reuse or are closed (pursuant to one of the exemptions)." + "description": "The URL of the public release repository for open source repositories. This field is not required for repositories that are only available as government-wide reuse or are closed (pursuant to one of the exemptions). It can be listed as 'private' for repositories that are closed." }, "projectURL": { "type": "string", @@ -304,9 +304,9 @@ "type": "string", "description": "Level of security categorization assigned to an information system under the Federal Information Security Modernization Act (FISMA): https://security.cms.gov/learn/federal-information-security-modernization-act-fisma", "enum": [ - "Low", - "Moderate", - "High" + "low", + "moderate", + "high" ] }, "group": { @@ -336,10 +336,10 @@ "items": { "type": "string", "enum": [ - "Policy", - "Operational", - "Medicare", - "Medicaid" + "policy", + "operational", + "medicare", + "medicaid" ] }, "description": "Healthcare-related subset" @@ -349,9 +349,9 @@ "items": { "type": "string", "enum": [ - "Providers", - "Patients", - "Government" + "providers", + "patients", + "government" ] }, "description": "Types of users who interact with the software" @@ -403,4 +403,4 @@ "maturityModelTier" ], "additionalProperties": false -} +} \ No newline at end of file diff --git a/schemas/gov/schema.json b/schemas/gov/schema.json new file mode 100644 index 0000000..e01857a --- /dev/null +++ b/schemas/gov/schema.json @@ -0,0 +1,185 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "code.json metadata", + "description": "A metadata standard for software repositories", + "type": "object", + "properties": { + "items": { + "name": { + "type": "string", + "description": "Name of the project or software" + }, + "description": { + "type": "string", + "description": "A short description of the project. It should be a single line containing a single sentence. Maximum 150 characters are allowed.", + "maxLength": 150 + }, + "permissions": { + "type": "object", + "description": "An object containing description of the usage/restrictions regarding the release", + "properties": { + "licenses": { + "type": "array", + "description": "License(s) for the release", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "CC0-1.0", + "Apache-2.0", + "MIT", + "MPL-2.0", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-only", + "LGPL-3.0-only", + "BSD-2-Clause", + "BSD-3-Clause", + "EPL-2.0", + "Other", + "None" + ], + "description": "An abbreviation for the name of the license" + }, + "URL": { + "type": "string", + "format": "uri", + "description": "The URL of the release license in the repository" + } + }, + "required": [ + "name", + "URL" + ] + } + }, + "usageType": { + "type": "string", + "description": "A list of enumerated values which describes the usage permissions for the release: (1) openSource: Open source; (2) governmentWideReuse: Government-wide reuse; (3) exemptByLaw: The sharing of the source code is restricted by law or regulation, including—but not limited to—patent or intellectual property law, the Export Asset Regulations, the International Traffic in Arms Regulation, and the Federal laws and regulations governing classified information; (4) exemptByNationalSecurity: The sharing of the source code would create an identifiable risk to the detriment of national security, confidentiality of Government information, or individual privacy; (5) exemptByAgencySystem: The sharing of the source code would create an identifiable risk to the stability, security, or integrity of the agency’s systems or personnel, (6) exemptByAgencyMission: The sharing of the source code would create an identifiable risk to agency mission, programs, or operations; (7) exemptByCIO: The CIO believes it is in the national interest to exempt sharing the source code; (8) exemptByPolicyDate: The release was created prior to the M-16-21 policy (August 8, 2016)", + "enum": [ + "openSource", + "governmentWideReuse", + "exemptByLaw", + "exemptByNationalSecurity", + "exemptByAgencySystem", + "exemptByAgencyMission", + "exemptByCIO", + "exemptByPolicyDate" + ], + "additionalProperties": false + }, + "exemptionText": { + "type": [ + "string", + "null" + ], + "description": "If an exemption is listed in the 'usageType' field, this field should include a one- or two- sentence justification for the exemption used." + } + }, + "additionalProperties": false, + "required": [ + "licenses", + "usageType" + ] + }, + "repositoryURL": { + "type": "string", + "format": "uri", + "description": "The URL of the public release repository for open source repositories. This field is not required for repositories that are only available as government-wide reuse or are closed (pursuant to one of the exemptions). It can be listed as 'private' for repositories that are closed." + }, + "repositoryVisibility": { + "type": "string", + "enum": [ + "public", + "private" + ], + "description": "Visibility of repository" + }, + "laborHours": { + "type": "number", + "description": "Labor hours invested in the project. Calculated using COCOMO measured by the SCC tool: https://github.com/boyter/scc?tab=readme-ov-file#cocomo" + }, + "reuseFrequency": { + "type": "object", + "description": "Measures frequency of code reuse in various forms. (e.g. forks, downloads, clones)", + "properties": { + "forks": { + "type": "integer" + }, + "clones": { + "type": "integer" + } + }, + "additionalProperties": true + }, + "maintenance": { + "type": "string", + "description": "The dedicated staff that keeps the software up-to-date, if any", + "enum": [ + "internal", + "contract", + "community", + "none" + ] + }, + "contractNumber": { + "type": "string", + "description": "Contract number" + }, + "tags": { + "type": "array", + "description": "Topics and keywords associated with the project to improve search and discoverability", + "items": { + "type": "string" + } + }, + "contact": { + "type": "object", + "description": "Point of contact for the release", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the point of contact" + }, + "name": { + "type": "string", + "description": "Name of the point of contact" + } + } + }, + "feedbackMechanisms": { + "type": "array", + "description": "Methods a repository receives feedback from the community. Default value is the URL to GitHub repository issues page.", + "items": { + "type": "string" + } + }, + "AIUseCaseInventory": { + "type": "boolean", + "description": "Is the software included in the agency's AI use case inventory?" + } + } + }, + "required": [ + "name", + "description", + "longDescription", + "permissions", + "repositoryURL", + "repositoryVisibility", + "laborHours", + "reuseFrequency", + "languages", + "maintenance", + "contractNumber", + "tags", + "contact", + "feedbackMechanisms", + "AIUseCaseInventory" + ], + "additionalProperties": false +}