| <div x-data="alpineSources()" x-init="getSources()" id="sourcePage" xmlns:x-bind="http://www.w3.org/1999/xhtml" |
| xmlns:x-on="http://www.w3.org/1999/xhtml"> |
| |
| <!-- PAGE HEADER --> |
| <div x-ref="headerSources"> |
| <h2>Fact Sources</h2> |
| <p> |
| Facts are identifiable pieces of data, collected by agents or loaded when the server starts. |
| A source is a collection of facts. Rules are boundaries to ensure specific traits cannot be used. |
| </p> |
| </div> |
| <hr> |
| |
| <!-- SOURCES TABLE --> |
| <div class="columns"> |
| <!-- LEFT SIDEBAR --> |
| <div class="column m-0 is-2 sidebar"> |
| <label x-show="false" class="label">Select a source </label> |
| <div class="select is-small is-fullwidth mb-2"> |
| <select x-on:change="getSource()" x-model="selectedSourceId"> |
| <option value="" default disabled selected>Select a source...</option> |
| <template x-for="source of sources" :key="source.id"> |
| <option x-bind:value="source.id" x-text="source.name"></option> |
| </template> |
| </select> |
| </div> |
| <button type="button" class="button is-primary is-small is-fullwidth mb-4" @click="createSource()"><span |
| class="icon"><em class="fas fa-plus"></em></span><span>Create Source</span></button> |
| |
| <label for="sourceName" x-show="false" class="label">Source Name</label> |
| |
| <div class="has-text-centered" x-show="selectedSourceId && !isEditingSourceName"> |
| <h3 x-text="selectedSource.name" class="break-anywhere pointer tooltip has-tooltip-arrow mt-0 mb-5" |
| data-tooltip="Click to edit" @click="isEditingSourceName = true"></h3> |
| </div> |
| <input x-show="selectedSourceId && isEditingSourceName" |
| @click.outside="isEditingSourceName = false; saveSourceName()" |
| id="sourceName" class="input mb-5" placeholder="Enter a source name" x-model="selectedSource.name"> |
| |
| |
| <template x-if="!selectedSourceId"> |
| <p class="is-flex is-justify-content-center is-italic mt-2">Select a fact source to get started.</p> |
| </template> |
| |
| <div class="control-buttons is-flex is-fullwidth is-flex-direction-column"> |
| <button class="button is-small mb-2" |
| x-bind:disabled="!selectedSourceId" |
| @click="openModal = { isOpen: true, name: 'Add From Ability' }; filterAbilities()"> |
| <span class="icon"><em class="fas fa-plus"></em></span> |
| <span>Add Facts from Ability</span> |
| </button> |
| <button class="button is-small mb-2" |
| x-bind:disabled="!selectedSourceId" |
| @click="openModal = { isOpen: true, name: 'Add From Adversary' }; getAdversaries();"> |
| <span class="icon"><em class="fas fa-user-plus"></em></span> |
| <span>Add Facts from Adversary</span> |
| </button> |
| <button class="button is-small mb-2" @click="duplicateSource(selectedSource)" |
| x-bind:disabled="!selectedSourceId"> |
| <span class="icon"><em class="fas fa-copy"></em></span> |
| <span>Duplicate Source</span> |
| </button> |
| <button class="button is-primary is-small mb-2" @click="downloadSource()" |
| x-bind:disabled="!selectedSourceId"> |
| <span class="icon"><em class="fas fa-arrow-down"></em></span> |
| <span>Download Source</span> |
| </button> |
| <div class="hr"></div> |
| <button class="button is-small mb-2 is-danger is-outlined" |
| @click="openModal = { isOpen: true, name: 'Delete Source' }"> |
| <span class="icon"><em class="fas fa-trash"></em></span> |
| <span>Delete Source</span> |
| </button> |
| </div> |
| </div> |
| |
| <template x-if="selectedSourceId"> |
| <!-- RIGHT SIDEBAR --> |
| <div class="column m-0 pt-0 sources-view is-10"> |
| <!-- TOPBAR CHIPS--> |
| <div class="is-flex is-align-items-center"> |
| <template x-for="chip of ['facts', 'rules', 'relationships']"> |
| <button class="is-small button is-rounded m-1" |
| x-on:click="inView = [chip]; addNewCard = false; editCard = ''; sortAToZ = false;" |
| x-text="chip" x-bind:class="{'is-primary': inView.includes(chip)}"></button> |
| </template> |
| <button class="button is-rounded is-small" @click="sortDisplay(); sortAToZ = !sortAToZ;"><em class="fas fa-sort"></em> |
| </button> |
| </div> |
| |
| <template x-if="inView.includes('facts')"> |
| <!-- FACTS --> |
| <div class="mt-2 is-flex is-flex-wrap-wrap is-align-content-flex-start"> |
| <div class="box sources add-new mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center" |
| x-on:click="addNewCard = true; editCard = null;"> |
| <p class="is-italic">+ new fact</p> |
| </div> |
| <template x-if="addNewCard"> |
| <div x-data="{newFact: {trait: '', tags: [] }}" |
| class="box sources mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column"> |
| <form> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-fact-trait">Trait</label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <input id="new-fact-trait" class="input is-small" placeholder="fact trait" |
| x-model="newFact.trait"> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="field is-flex is-horizontal is-flex is-align-items-center is-flex-direction-column"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label pt-2">Values</label> |
| </div> |
| <div class="field-body"> |
| <template x-for="(tag, index) in newFact.tags"> |
| <div id="new-fact-value" class="tags has-addons mb-0"> |
| <span class="is-italic tag is-light break-spaces"> |
| <input type="text" class="tag-input w-110-px" |
| placeholder="value" |
| x-model="tag.value"> |
| </span> |
| <span class="tag is-white break-spaces"><input |
| class="tag-number-input" type="number" |
| pattern="[0-9]*" |
| min="0" |
| placeholder="1" |
| x-model="tag.score"> |
| <button @click="newFact.tags.splice(index, 1)" |
| class="delete is-small"></button> |
| </span> |
| </div> |
| </template> |
| <div class="is-flex is-justify-content-center"> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new value" |
| @click="newFact.tags.push({trait: newFact.trait, value: '', score: 1})"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| </div> |
| </form> |
| |
| <div class="mt-3"> |
| <button class="button is-small" @click="addNewCard = false"> |
| <span>Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" @click="addFacts(newFact.tags)" |
| x-bind:disabled="!newFact.trait"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </div> |
| </template> |
| <template x-for="(fact, index) in filteredFacts" :key="fact.trait"> |
| <div class="box sources mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column"> |
| <div class="w-100" @click="($event) => editCard = ($event.target.id !== 'cancel-edit-fact') ? fact.trait : null" |
| x-data="{editedValues: {}, editedScores: {}, newTags: [], editedNames: {}}"> |
| <div> |
| <p x-show="editCard !== fact.trait" x-bind:class="{'has-background-warning has-text-black': editCard === fact.trait}" class="p-1 has-text-centered" x-text="fact.trait"></p> |
| <input class="input is-small" x-show="editCard === fact.trait" x-model="editedNames[fact.trait]" x-init="editedNames[fact.trait] = fact.trait"> |
| <template x-if="editCard === fact.trait"> |
| <button class="button is-danger is-outlined is-small delete-card-button" @click="deleteFact(fact.trait)"><span class="icon"><em class="fas fa-trash"></em></span></button> |
| </template> |
| </div> |
| <div class="is-flex is-justify-content-center is-flex-direction-row"> |
| <div class="is-flex is-justify-content-center is-align-items-center is-flex-direction-column"> |
| <label x-show="fact.scores" class="label mb-0 is-size-7" |
| x-text="'Values'"></label> |
| <div class="field is-grouped is-grouped-multiline is-flex is-align-items-center is-justify-content-center"> |
| <template x-for="(score) in fact.scores" |
| :key="fact.trait + score.score" |
| x-init="$watch('filteredFacts', updated => {if (updated && updated[index]) fact.scores = updated[index].scores;})"> |
| <div> |
| <div class="is-flex is-justify-content-center is-align-items-center is-flex-direction-row is-flex-wrap-wrap"> |
| <span x-show="editCard !== fact.trait" |
| class="tag break-spaces is-info" |
| x-text="score.score + (score.score > 1 ? ' pts:' :' pt:')"></span> |
| <template x-for="(val, index) in score.values" :key="val.id"> |
| <div> |
| <template x-if="editCard !== fact.trait"> |
| <span class="tag is-light break-spaces editable-tag" |
| x-text="(val.value ? val.value : '--')"></span> |
| </template> |
| <template x-if="editCard === fact.trait"> |
| <div> |
| <div class="tags has-addons mb-0"> |
| <span class="is-italic tag is-light break-spaces"> |
| <input type="text" class="tag-input" |
| x-bind:value="val.value" |
| x-on:change="val.value = editedValues[score.unique] = $event.target.value;"> |
| </span> |
| <span class="tag is-white break-spaces"> |
| <input class="tag-number-input" |
| type="number" |
| min="0" |
| pattern="[0-9]*" |
| placeholder="1" |
| x-bind:value="score.score"> |
| <button @click="score.values.splice(index, 1) && deleteFactValue(fact.trait, score.unique, val.value)" |
| class="delete is-small"></button> |
| </span> |
| </div> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| </div> |
| </template> |
| </div> |
| <template x-if="editCard === fact.trait"> |
| <div> |
| <template x-for="(tag, index) in newTags"> |
| <div class="tags has-addons mb-0"> |
| <span class="is-italic tag is-light break-spaces"> |
| <input type="text" class="tag-input" |
| placeholder="value" |
| x-model="tag.value"> |
| </span> |
| <span class="tag is-white break-spaces"><input |
| class="tag-number-input" type="number" |
| pattern="[0-9]*" |
| min="0" |
| placeholder="1" |
| x-model="tag.score"> |
| <button @click="newTags.splice(index, 1)" |
| class="delete is-small"></button> |
| </span> |
| </div> |
| </template> |
| <div class="is-flex is-justify-content-center"> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new value" |
| @click="newTags.push({trait: fact.trait, value: '', score: 1})"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| </template> |
| <template x-if="editCard === fact.trait"> |
| <div class="mt-3"> |
| <button class="button is-small" @click="editCard = null;"> |
| <span id="cancel-edit-fact">Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" |
| @click="updateFact(editedValues, editedScores, newTags, editedNames); newTags = []; $event.stopPropagation();"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </template> |
| </div> |
| </div> |
| </div> |
| </div> |
| </template> |
| </div> |
| </template> |
| |
| <!-- RULES --> |
| <template x-if="inView.includes('rules')"> |
| <div class="mt-2 is-flex is-flex-wrap-wrap is-align-content-flex-start"> |
| <div class="box sources add-new mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center" |
| x-on:click="addNewCard = true; editCard = null;"> |
| <p class="is-italic">+ new rule</p> |
| </div> |
| <template x-if="addNewCard"> |
| <div class="box sources mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column" |
| x-data="{newRule: {trait: '', allow: [], deny: []}, addNewAllow: false, addNewDeny: false, newAllow: '', newDeny: ''}"> |
| <form> |
| <div class="field is-horizontal is-flex is-align-items-center"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-rule-trait">Trait</label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <input id="new-rule-trait" class="input is-small" placeholder="fact trait" |
| x-model="newRule.trait"> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="field is-horizontal mb-3 is-flex is-align-items-center"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-rule-allow">Allow</label> |
| </div> |
| <div class="field-body is-flex is-flex-direction-row is-flex-wrap-wrap" id="new-rule-allow"> |
| <template x-for="(match, index) in [...new Set(newRule.allow)]" :key="match"> |
| <span class="tag is-success m-1 break-spaces editable-tag"> |
| <span x-text="match"></span> |
| <button @click="newRule.allow.splice(newRule.allow.findIndex(v => v === match), 1)" class="delete is-small"></button> |
| </span> |
| </template> |
| <template x-if="addNewAllow"> |
| <span class="tag is-light break-spaces"> |
| <input class="tag-number-input" placeholder=".*" x-model="newAllow"/> |
| </span> |
| </template> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new rule" |
| @click="if (newAllow) {newRule.allow.push(newAllow); newAllow = '';}; addNewAllow = true"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| <div class="field is-horizontal is-flex is-align-items-center mb-3"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-rule-deny">Deny</label> |
| </div> |
| <div class="field-body is-flex is-flex-direction-row is-flex-wrap-wrap" id="new-rule-deny"> |
| <template x-for="(match, index) in [...new Set(newRule.deny)]" :key="match"> |
| <span class="tag is-danger m-1 break-spaces editable-tag"> |
| <span x-text="match"></span> |
| <button @click="newRule.deny.splice(newRule.deny.findIndex(v => v === match), 1)" class="delete is-small"></button> |
| </span> |
| </template> |
| <template x-if="addNewDeny"> |
| <span class="tag is-light break-spaces"> |
| <input class="tag-number-input" placeholder=".*" x-model="newDeny"/> |
| </span> |
| </template> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new rule" |
| @click="if (newDeny) {newRule.deny.push(newDeny); newDeny = '';}; addNewDeny = true"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| </form> |
| <div> |
| <button class="button is-small" @click="addNewCard = false"> |
| <span>Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" |
| @click="if (newAllow) {newRule.allow.push(newAllow); newAllow = '';}; if (newDeny) {newRule.deny.push(newDeny); newDeny = '';}; addRule(newRule)" |
| x-bind:disabled="!newRule.trait"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </div> |
| </template> |
| <template x-for="(rule, index) in filteredRules" :key="rule.trait"> |
| <div class="box sources mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column" @click.self="editCard = rule.trait"> |
| <template x-if="editCard !== rule.trait"> |
| <div> |
| <div class="is-flex is-justify-content-center mb-2"> |
| <p x-text="rule.trait"></p> |
| </div> |
| <div class="is-size-7"> |
| <template x-if="rule.allow_matches.length > 0"> |
| <label class="label mb-0 is-inline-block">Allow: </label> |
| </template> |
| <template x-for="(match, index) in rule.allow_matches" :key="match"> |
| <p class="tag is-success m-1 break-spaces editable-tag" |
| x-text="match"> |
| </p> |
| </template> |
| </div> |
| <div class="is-size-7"> |
| <template x-if="rule.deny_matches.length > 0"> |
| <label class="label mb-0 is-inline-block">Deny: </label> |
| </template> |
| <template x-for="(match, index) in rule.deny_matches" :key="match"> |
| <p class="tag is-danger m-1 break-spaces editable-tag" |
| x-text="match"> |
| </p> |
| </template> |
| </div> |
| </div> |
| </template> |
| <template x-if="editCard === rule.trait"> |
| <div x-data="{updatedRule: {trait: rule.trait, allow: rule.allow_matches, deny: rule.deny_matches}, addNewAllow: false, addNewDeny: false, newAllow: '', newDeny: ''}"> |
| <div class="is-flex is-justify-content-center mb-2"> |
| <p class="has-background-warning has-text-black p-1" x-text="rule.trait"></p> |
| <button class="button is-danger is-outlined is-small delete-card-button" @click="deleteRule(rule.trait)"> |
| <span class="icon"><em class="fas fa-trash"></em></span> |
| </button> |
| </div> |
| <form> |
| <div class="field is-horizontal mb-3 is-flex is-align-items-center"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="edit-rule-allow">Allow</label> |
| </div> |
| <div class="field-body is-flex is-flex-direction-row is-flex-wrap-wrap" id="edit-rule-allow"> |
| <template x-for="(match, index) in [...new Set(updatedRule.allow)]" :key="match"> |
| <span class="tag is-success m-1 break-spaces editable-tag"> |
| <span x-text="match"></span> |
| <button @click="updatedRule.allow.splice(updatedRule.allow.findIndex(v => v === match), 1)" class="delete is-small"></button> |
| </span> |
| </template> |
| <template x-if="addNewAllow"> |
| <span class="tag is-light break-spaces"> |
| <input class="tag-number-input" placeholder=".*" x-model="newAllow"/> |
| </span> |
| </template> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new rule" |
| @click="if (newAllow) {updatedRule.allow.push(newAllow); newAllow = '';}; addNewAllow = true"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| <div class="field is-horizontal is-flex is-align-items-center mb-3"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="edit-rule-deny">Deny</label> |
| </div> |
| <div class="field-body is-flex is-flex-direction-row is-flex-wrap-wrap" id="edit-rule-deny"> |
| <template x-for="(match, index) in [...new Set(updatedRule.deny)]" :key="match"> |
| <span class="tag is-danger m-1 break-spaces editable-tag"> |
| <span x-text="match"></span> |
| <button @click="updatedRule.deny.splice(updatedRule.deny.findIndex(v => v === match), 1)" class="delete is-small"></button> |
| </span> |
| </template> |
| <template x-if="addNewDeny"> |
| <span class="tag is-light break-spaces"> |
| <input class="tag-number-input" placeholder=".*" x-model="newDeny"/> |
| </span> |
| </template> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new rule" |
| @click="if (newDeny) {updatedRule.deny.push(newDeny); newDeny = '';}; addNewDeny = true"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| </div> |
| </form> |
| <div class="is-flex is-justify-content-center"> |
| <button class="button is-small" @click="editCard = ''"> |
| <span>Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" |
| @click="if (newAllow) {updatedRule.allow.push(newAllow); newAllow = '';}; if (newDeny) {updatedRule.deny.push(newDeny); newDeny = '';}; updateRules(updatedRule)"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| </template> |
| |
| <!-- RELATIONSHIPS --> |
| <template x-if="inView.includes('relationships')"> |
| <div class="mt-2 is-flex is-flex-wrap-wrap is-align-content-flex-start" |
| x-data="{newRelationships: [], newRelationship: {edge: '', source: {}, target: {}}}"> |
| <div class="box sources add-new mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center" |
| x-on:click="addNewCard = true; editCard = null;"> |
| <p class="is-italic">+ new relationship</p> |
| </div> |
| <template x-if="addNewCard"> |
| <div class="box sources add-new mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column"> |
| <div class="w-90"> |
| <form> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-relationship-edge">Edge</label> |
| </div> |
| <div class="field-body"> |
| <input id="new-relationship-edge" class="input is-small" placeholder="edge" |
| x-model="newRelationship.edge"> |
| </div> |
| </div> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-relationship-source">Source</label> |
| </div> |
| <div class="field-body"> |
| <input id="new-relationship-source" class="input is-small" |
| placeholder="source trait" |
| x-model="newRelationship.source.trait"> |
| </div> |
| </div> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="new-relationship-target">Target</label> |
| </div> |
| <div class="field-body"> |
| <input id="new-relationship-target" class="input is-small mb-5" |
| placeholder="target trait" |
| x-model="newRelationship.target.trait"> |
| </div> |
| </div> |
| </form> |
| </div> |
| <div> |
| <button class="button is-small" @click="addNewCard = false"> |
| <span>Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" |
| @click="addRelationship(newRelationship); newRelationship = {edge: '', source: {}, target: {}}" |
| x-bind:disabled="!newRelationship.edge"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </div> |
| </template> |
| <template x-for="(relationship, index) in filteredRelationships" :key="relationship.trait"> |
| <div class="box sources mb-2 mr-2 p-3 is-flex is-justify-content-center is-align-items-center is-flex-direction-column" @click.self="editCard = relationship.trait"> |
| <div> |
| <div class="is-flex is-justify-content-center mb-2"> |
| <p x-bind:class="{'has-background-warning has-text-black': editCard === relationship.trait}" class="p-1" |
| x-text="relationship.trait"></p> |
| <template x-if="editCard === relationship.trait"> |
| <button class="button is-danger is-outlined is-small delete-card-button" @click="deleteRelationship(relationship.trait)"> |
| <span class="icon"><em class="fas fa-trash"></em></span> |
| </button> |
| </template> |
| </div> |
| <div class="is-flex is-justify-content-center is-align-items-center is-flex-direction-column"> |
| <template x-for="(rel, index) in relationship.relationships" |
| :key="rel.from+index"> |
| <div class="is-size-7" x-bind:class="{'pr-2': index > 0}"> |
| <span class="tag is-outlined is-light is-italic m-1 break-spaces" |
| x-text="rel.from"></span> |
| <em class="has-text-white fas fa-arrow-right"></em> |
| <span class="tag is-outlined is-light is-italic m-1 break-spaces" |
| x-text="rel.to"></span> |
| </div> |
| </template> |
| </div> |
| </div> |
| <template x-if="editCard === relationship.trait"> |
| <div class="is-flex is-flex-direction-column is-flex-wrap-wrap is-align-items-center"> |
| <template x-for="(newTag, index) in newRelationships" |
| :key="'new'+relationship.trait+index"> |
| <div class="is-size-7" x-bind:class="{'pr-2': index > 0}"> |
| <div class="is-grouped is-grouped-multiline is-flex is-justify-content-center is-align-items-center mb-2"> |
| <span class="tag is-light is-italic m-1 break-spaces w-35"> |
| <input type="text" class="tag-input w-100" |
| placeholder="source trait" |
| x-model="newTag.source.trait"> |
| </span> |
| <em class="has-text-white fas fa-arrow-right"></em> |
| <span class="tag is-light is-italic m-1 break-spaces w-35"> |
| <input type="text" class="tag-input w-100" |
| placeholder="target trait" |
| x-model="newTag.target.trait"> |
| </span> |
| <button @click="newRelationships.splice(index, 1)" |
| class="button delete is-small"></button> |
| </div> |
| </div> |
| </template> |
| <div class="is-flex is-justify-content-center mb-2"> |
| <span class="tag is-white break-spaces" |
| data-tooltip="Add a new relationship" |
| @click="newRelationships.push({edge: relationship.trait, source:{trait:''}, target:{trait:''}})"> |
| <em class="fas fa-plus"></em> |
| </span> |
| </div> |
| <div> |
| <button class="button is-small" @click="editCard = ''"> |
| <span>Cancel</span> |
| </button> |
| <button class="button is-small is-primary ml-2" |
| @click="updateRelationship(newRelationships); editCard = ''; newRelationships = []"> |
| <span class="icon"><i class="fas fa-save"></i></span> |
| <span>Save</span> |
| </button> |
| </div> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| </template> |
| </div> |
| |
| |
| <!-- MODALS --> |
| <div class="modal" x-bind:class="{ 'is-active': openModal.isOpen }"> |
| <div class="modal-background" @click="openModal.isOpen = false"></div> |
| <template x-if="openModal.name.includes('Delete')"> |
| <div class="modal-card" style="z-index: 20"> |
| <header class="modal-card-head"> |
| <p class="modal-card-title">Delete Fact Source?</p> |
| </header> |
| <section class="modal-card-body"> |
| <p> |
| Are you sure you want to delete this fact source? This cannot be undone. |
| </p> |
| </section> |
| <footer class="modal-card-foot"> |
| <nav class="level"> |
| <div class="level-left"> |
| <div class="level-item"> |
| <button class="button is-small" @click="openModal.isOpen = false">Cancel</button> |
| </div> |
| </div> |
| <div class="level-right"> |
| <div class="level-item"> |
| <button class="button is-danger is-small" |
| @click="deleteSource(); openModal.isOpen = false;"> |
| <span class="icon"><em class="fas fa-trash"></em></span> |
| <span class="has-text-weight-bold">Delete Source</span> |
| </button> |
| <div> |
| </div> |
| </nav> |
| </footer> |
| </div> |
| </template> |
| <template x-if="openModal.name.includes('Ability')" x-data="{selectedAbility: null}"> |
| <div class="modal-card" style="z-index: 20"> |
| <header class="modal-card-head"> |
| <p class="modal-card-title">Add Fact from Ability </p> |
| </header> |
| <section class="modal-card-body"> |
| <form> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small"> |
| <label class="label"><span class="icon is-small"><em |
| class="fas fa-search"></em></span></label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <input class="input is-small" x-model="abilitySearch.query" |
| placeholder="Search for an ability..." x-on:keyup="filterAbilities()"> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label">Tactic</label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <div class="select is-small is-fullwidth"> |
| <select id="link-tactic" x-model="abilitySearch.tactic" |
| x-on:change="filterAbilities()"> |
| <option default value="">All tactics</option> |
| <template x-for="tactic in tactics" :key="tactic"> |
| <option x-text="tactic"></option> |
| </template> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label">Technique</label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <div class="select is-small is-fullwidth"> |
| <select id="link-technique" x-model="abilitySearch.technique" |
| x-on:change="filterAbilities()"> |
| <option default value="">All techniques</option> |
| <template x-for="technique in techniques" :key="technique"> |
| <option x-text="technique"></option> |
| </template> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </form> |
| <hr> |
| <div class="is-flex is-justify-content-space-between mb-2"> |
| <strong x-show="filteredAbilities.length">Select an ability</strong> |
| </div> |
| <template x-for="ability in filteredAbilities" :key="ability.ability_id"> |
| <div class="box selectable mb-2 mr-2 p-3 has-background-black-ter" |
| x-bind:class="{'selected': selectedAbility === ability}" |
| @click="selectedAbility = ability"> |
| <p class="mb-0"> |
| <span class="has-text-weight-bold" x-text="ability.name"></span> |
| <span x-text="`${ability.technique_id}`"></span> |
| </p> |
| <div class="is-flex is-align-items-center"> |
| <label class="is-italic is-size-7 break-spaces" for="ability-fact-score">Score: </label> |
| <input class="input has-background-grey-darker is-small m-2 w-50-px" |
| id="ability-fact-score" x-model="ability.score" type="number" required |
| pattern="[0-9]*" min="0" placeholder="1"/> |
| </div> |
| <p x-show="ability.factTraits" class="is-italic is-size-7 break-anywhere"> |
| <span>Facts: </span> |
| <template x-for="fact in ability.factTraits"> |
| <span class="has-background-warning has-text-black mr-2 px-1" |
| x-text="`${fact}`"></span> |
| </template> |
| </p> |
| <p class="is-size-7" x-text="ability.description"></p> |
| </div> |
| </template> |
| <template x-if="filteredAbilities.length === 0"> |
| <p class="is-size-7 is-italic ml-4 text-align-center">No results, try |
| <button class="no-style button-link is-italic has-text-white is-size-7 px-0" |
| alt="click to reset search" @click="resetSearch()"> |
| resetting search |
| </button> |
| ? |
| </p> |
| </template> |
| </section> |
| <footer class="modal-card-foot"> |
| <nav class="level"> |
| <div class="level-left"> |
| <div class="level-item"> |
| <button class="button is-small" @click="openModal.isOpen = false">Done</button> |
| </div> |
| </div> |
| <div class="level-right"> |
| <div class="level-item"> |
| <button x-bind:disabled="!selectedAbility" class="button is-primary is-small" |
| @click="addFromAbility(selectedAbility.factTraits, selectedAbility.score); openModal.isOpen = false; inView = ['facts'];"> |
| <span>Add Facts</span> |
| </button> |
| <div> |
| </div> |
| </nav> |
| </footer> |
| </div> |
| </template> |
| <template x-if="openModal.name.includes('Adversary')" x-data="{selectedAdversary: ''}"> |
| <div class="modal-card" style="z-index: 20"> |
| <header class="modal-card-head"> |
| <p class="modal-card-title">Add Fact from Adversary</p> |
| </header> |
| <section class="modal-card-body"> |
| <form> |
| <div class="field is-horizontal"> |
| <div class="field-label is-small is-flex-grow-2"> |
| <label class="label" for="sourcesAdversaryDropdown">Adversary</label> |
| </div> |
| <div class="field-body"> |
| <div class="field"> |
| <div class="control"> |
| <div class="select is-small is-fullwidth"> |
| <select id="sourcesAdversaryDropdown" |
| x-on:change="filteredAbilities = []; getAdversaryFacts(selectedAdversary)" |
| x-model="selectedAdversary"> |
| <option value="" default disabled selected>Select one...</option> |
| <template x-for="adversary of adversaries" |
| :key="adversary.adversary_id"> |
| <option x-bind:value="adversary.adversary_id" |
| x-text="adversary.name" |
| x-bind:title="adversary.description"></option> |
| </template> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </form> |
| <template x-if="selectedAdversary"> |
| <span> |
| <hr> |
| <div class="is-flex is-justify-content-space-between mb-2"> |
| <strong x-show="selectedAdversary" x-text="'Adversary Abilities'"></strong> |
| </div> |
| <template x-if="filteredAbilities.length < 1"> |
| <div class="box mb-2 mr-2 p-3 has-background-black-ter"> |
| <p class="text-align-center is-italic">This adversary has no facts available.</p> |
| </div> |
| </template> |
| <template x-if="filteredAbilities.length > 0"> |
| <span> |
| <template x-for="(ability, index) in filteredAbilities" :key="index"> |
| <div class="box mb-2 mr-2 p-3 has-background-black-ter"> |
| <p> |
| <span class="has-text-weight-bold" x-text="ability.name"></span> |
| <span x-text="`${ability.technique_id}`"></span> |
| </p> |
| <div class="is-flex is-align-items-center"> |
| <label class="is-italic is-size-7" |
| for="adversary-fact-score">Score: </label> |
| <input class="input has-background-grey-darker is-small m-2 w-50-px" |
| id="adversary-fact-score" x-model="ability.score" type="number" |
| required pattern="[0-9]*" min="0" placeholder="1"/> |
| </div> |
| <p x-show="ability.factTraits" class="is-italic is-size-7 break-anywhere"> |
| <span>Facts: </span> |
| <template x-for="fact in ability.factTraits"> |
| <span class="has-background-warning has-text-black mr-2 px-1" |
| x-text="`${fact}`"></span> |
| </template> |
| </p> |
| <p class="is-size-7" x-text="ability.description"></p> |
| </div> |
| </template> |
| </span> |
| </template> |
| </span> |
| </template> |
| </section> |
| <footer class="modal-card-foot"> |
| <nav class="level"> |
| <div class="level-left"> |
| <div class="level-item"> |
| <button class="button is-small" @click="openModal.isOpen = false">Done</button> |
| </div> |
| </div> |
| <div class="level-right"> |
| <div class="level-item"> |
| <button x-bind:disabled="filteredAbilities.length < 1" |
| class="button is-primary is-small" |
| @click="addFromAdversary(); openModal.isOpen = false; inView = ['facts']"> |
| <span>Add All Facts</span> |
| </button> |
| <div> |
| </div> |
| </nav> |
| </footer> |
| </div> |
| </template> |
| </div> |
| </div> |
| |
| <script> |
| function alpineSources() { |
| return { |
| sources: [], |
| inView: ['facts'], |
| sortAToZ: true, |
| isEditingSourceName: false, |
| |
| // filteredFacts = [{trait: '', scores: {1: [{score: 1, unique: '', value: ''}], 2: [{score: 2, unique: '', value: ''}]}}] |
| filteredFacts: [], |
| // filteredRules = [{trait: '', allow_matches: [{value: '', allow: true}], deny_matches: [{value: '', allow: false}]}] |
| filteredRules: [], |
| // filteredRelationships = [{trait: '', relationships: [{from: '', to: ''}, {from: '', to: ''}]}] |
| filteredRelationships: [], |
| |
| selectedSource: { |
| name: '', |
| relationships: [] |
| }, |
| selectedSourceId: '', |
| |
| openModal: { |
| isOpen: false, |
| name: '' |
| }, |
| tactics: [], |
| techniques: [], |
| filteredAbilities: [], |
| abilitySearch: { |
| query: '', |
| tactic: '', |
| technique: null |
| }, |
| abilities: [], |
| adversaries: [], |
| |
| addNewCard: false, |
| editCard: '', |
| |
| getSources() { |
| apiV2('GET', '/api/v2/sources').then((sources) => { |
| this.sources = sources.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase()); |
| this.getSource(); |
| }).catch((error) => { |
| toast('Error loading page', false); |
| console.error(error); |
| }); |
| }, |
| |
| // Gets selected source and modifies object for display (i.e. filteredFacts is a different object from the source.facts object from the rest API) |
| getSource() { |
| if (!this.selectedSourceId) return; |
| this.selectedSource = this.sources.find((source) => source.id === this.selectedSourceId); |
| this.filterSource(); |
| }, |
| |
| filterSource() { |
| const facts = {}; // bucket facts by 1) trait, 2) score |
| this.selectedSource.facts.forEach((f) => { |
| if (!(facts[f.trait])) { |
| const bucket = {}; |
| bucket[f.score] = { |
| score: f.score, |
| unique: f.unique, |
| values: [f.value.replaceAll(',', ', ')] |
| }; // score is defined twice because this object becomes an array later, and this keeps the score even after the {} is converted to an [] |
| facts[f.trait] = { |
| trait: f.trait, |
| scores: bucket |
| }; |
| } else { |
| if (!facts[f.trait].scores[f.score]) { |
| const bucket = { |
| score: f.score, |
| unique: f.unique, |
| values: [f.value.replaceAll(',', ', ')] |
| }; |
| facts[f.trait].scores[f.score] = bucket; |
| } else { |
| facts[f.trait].scores[f.score].values = facts[f.trait].scores[f.score].values.concat([f.value.replaceAll(',', ', ')]); |
| } |
| } |
| }); |
| |
| let rules = {}; |
| this.selectedSource.rules.forEach((rule) => { |
| if (rules[rule.trait]) { |
| if (rule.action === 'ALLOW') { |
| rules[rule.trait].allow_matches.push(rule.match); |
| } else if (rule.action === 'DENY') { |
| rules[rule.trait].deny_matches.push(rule.match); |
| } |
| } else { |
| if (rule.action === 'ALLOW') { |
| rules[rule.trait] = { |
| trait: rule.trait, |
| allow_matches: [rule.match], |
| deny_matches: [] |
| }; |
| } else if (rule.action === 'DENY') { |
| rules[rule.trait] = { |
| trait: rule.trait, |
| allow_matches: [], |
| deny_matches: [rule.match] |
| }; |
| } |
| } |
| }); |
| |
| let relationships = {}; |
| this.selectedSource.relationships.forEach((r) => { |
| if (relationships[r.edge]) { |
| relationships[r.edge].relationships.push({ |
| from: r.source.name, |
| to: r.target.name |
| }); |
| } else { |
| relationships[r.edge] = { |
| trait: r.edge, |
| relationships: [{ |
| from: r.source.name, |
| to: r.target.name |
| }] |
| }; |
| } |
| }); |
| |
| // Convert buckets {} into array for easy looping in template |
| this.filteredFacts = Object.keys(facts).map(key => { |
| const fact = facts[key]; |
| fact.scores = Object.keys(fact.scores).map(s => facts[key].scores[s]); |
| // Give each fact score value a unique id for easy deletion |
| fact.scores.forEach((score) => { |
| score.values.forEach((value, index) => { |
| score.values[index] = { |
| id: `${fact.trait}-${score.score}-${index}`, |
| value: value |
| }; |
| }); |
| }); |
| return fact; |
| }); |
| this.filteredRules = Object.keys(rules).map(key => rules[key]); |
| this.filteredRelationships = Object.keys(relationships).map(key => relationships[key]); |
| this.sortDisplay(); |
| }, |
| |
| sortDisplay() { |
| if (this.inView.length > 0) { |
| if (this.inView.includes('facts')) { |
| this.sortAToZ ? this.filteredFacts = this.filteredFacts.sort((a, b) => a.trait.toLowerCase() < b.trait.toLowerCase() ? -1 : 1) : |
| this.filteredFacts = this.filteredFacts.sort((a, b) => a.trait.toLowerCase() > b.trait.toLowerCase() ? -1 : 1); |
| } |
| if (this.inView.includes('rules')) { |
| this.sortAToZ ? this.filteredRules = this.filteredRules.sort((a, b) => a.trait.toLowerCase() < b.trait.toLowerCase() ? -1 : 1) : |
| this.filteredRules = this.filteredRules.sort((a, b) => a.trait.toLowerCase() > b.trait.toLowerCase() ? -1 : 1); |
| } |
| if (this.inView.includes('relationships')) { |
| this.sortAToZ ? this.filteredRelationships = this.filteredRelationships.sort((a, b) => a.trait.toLowerCase() < b.trait.toLowerCase() ? -1 : 1) : |
| this.filteredRelationships = this.filteredRelationships.sort((a, b) => a.trait.toLowerCase() > b.trait.toLowerCase() ? -1 : 1); |
| } |
| } |
| }, |
| |
| createSource() { |
| this.isEditingSourceName = true; |
| const newSource = { |
| name: 'New source', |
| facts: [], |
| rules: [], |
| relationships: [] |
| }; |
| apiV2('POST', '/api/v2/sources', newSource).then((src) => { |
| toast('Added new fact source', true); |
| this.isEditingSourceName = true; |
| this.sources.push(src); |
| this.selectedSourceId = src.id; |
| this.selectedSource = src; |
| this.getSource(); |
| this.$nextTick(() => { |
| document.getElementById('sourceName').focus(); |
| }); |
| }).catch((error) => { |
| toast('Error adding source', false); |
| console.error(error); |
| }); |
| }, |
| |
| getFilters(abilities) { |
| abilities.forEach((a) => { |
| this.tactics.push(a.tactic); |
| this.techniques.push(`${a.technique_id} | ${a.technique_name}`); |
| }); |
| this.tactics = [...new Set(this.tactics)].sort(); |
| this.techniques = [...new Set(this.techniques)].sort(); |
| }, |
| |
| filterAbilities() { |
| apiV2('GET', '/api/v2/abilities').then((abilities) => { |
| this.getFilters(abilities); |
| this.filteredAbilities = getFilteredAbilities(abilities, this.abilitySearch.query, this.abilitySearch.tactic, this.abilitySearch.technique); |
| this.getAbilityFacts(); |
| }).catch((error) => { |
| toast('There was an error getting abilities', false); |
| console.error(error); |
| }); |
| }, |
| |
| getAbilityFacts() { |
| let abilitiesWithFacts = []; |
| this.filteredAbilities.forEach((a) => { |
| let factTraits = []; |
| if (a.executors) { |
| a.executors.forEach((e) => { |
| if (e.command) { |
| const m = [...e.command.matchAll(/#{(.*?)}/gm)]; |
| if (m && m.length > 0) factTraits = factTraits.concat(m.map((variable) => variable[1])); // find all abilities where an executor has a command with a ${} fact variable |
| } |
| }); |
| if (factTraits.length > 0) { |
| abilitiesWithFacts.push({ |
| ...a, |
| score: 1, |
| factTraits: [...new Set(factTraits)] |
| }); |
| } |
| } |
| }); |
| this.filteredAbilities = abilitiesWithFacts; |
| }, |
| |
| addFromAbility(factTraits, score, fromAdversary = false) { // add all facts found from abilities |
| this.openModal.isOpen = false; |
| factTraits.forEach((val) => { |
| this.addFact({ |
| trait: val, |
| value: '', |
| score: score |
| }, true); |
| }); |
| if (!fromAdversary) toast('Added facts to source', true); |
| }, |
| |
| getAdversaries() { |
| apiV2('GET', '/api/v2/adversaries').then((adversaries) => { |
| this.adversaries = adversaries; |
| return apiV2('GET', '/api/v2/abilities'); |
| }).then((abilities) => { |
| this.abilities = abilities; |
| }).catch((error) => { |
| toast('Error getting adversaries', false); |
| console.error(`Error getting adversaries: ${error}`); |
| }); |
| }, |
| |
| getAdversaryFacts(selectedAdversaryID) { |
| const selectedAdversary = this.adversaries.find((adversary) => adversary.adversary_id === selectedAdversaryID); |
| this.filteredAbilities = selectedAdversary.atomic_ordering.map((ability_id) => ({ ...this.abilities.find((ability) => ability.ability_id === ability_id) })); |
| this.getAbilityFacts(); |
| }, |
| |
| addFromAdversary() { |
| this.filteredAbilities.forEach((a) => { |
| if (a.factTraits) { |
| this.addFromAbility(a.factTraits, a.score, true); |
| } |
| }); |
| }, |
| |
| duplicateSource(source) { |
| const newSource = { |
| ...source, |
| name: `${source.name} (copy)`, |
| id: '' |
| }; |
| apiV2('POST', '/api/v2/sources', newSource).then((src) => { |
| this.sources.push(src); |
| this.selectedSourceId = src.id; |
| this.selectedSource = src; |
| this.getSources(); |
| toast('Duplicated fact source', true); |
| }).catch((error) => { |
| toast('Error duplicating source', false); |
| console.error(error); |
| }); |
| }, |
| |
| updateFactSource(source = this.selectedSource) { |
| apiV2('PATCH', `/api/v2/sources/${this.selectedSource.id}`, source).then((r) => { |
| toast('Updated source', true); |
| this.selectedSource = r; |
| this.getSources(); |
| }).catch((error) => { |
| toast('Error loading page', false); |
| console.error(error); |
| }); |
| }, |
| |
| addFact(newFact, bulkAdd = false) { |
| const updatedSource = { ...this.selectedSource }; |
| updatedSource.facts.push(newFact); |
| apiV2('PUT', `/api/v2/sources/${this.selectedSource.id}`, updatedSource).then((res) => { |
| if (!bulkAdd) toast('Updated source', res); |
| this.getSources(); |
| this.addNewCard = false; |
| }).catch((error) => { |
| toast('Error adding fact', false); |
| console.error(error); |
| }); |
| }, |
| |
| addFacts(newFacts) { |
| newFacts.forEach(f => this.addFact(f, true)); |
| toast('Added new fact.', true); |
| }, |
| |
| updateFact(editedValues, editedScores, newTags, editedNames) { |
| // Do a comparison of facts and new facts--replace uniques with ones in the new one |
| let updated = this.selectedSource.facts.map((f) => { |
| if (!editedValues[f.unique] && !editedScores[f.unique] && !editedNames[f.name]) { |
| return f; // return original fact |
| } |
| let fact = { trait: f.trait }; |
| if (editedValues[f.unique]) { |
| fact.value = editedValues[f.unique]; |
| } else { |
| fact.value = f.value; |
| } |
| if (editedScores[f.unique]) { |
| fact.score = isNaN(editedScores[f.unique]) ? 1 : Number(editedScores[f.unique]); |
| } else { |
| fact.score = f.score; |
| } |
| if (editedNames[f.name]) { |
| fact.name = editedNames[f.name]; |
| fact.trait = editedNames[f.name]; |
| } else { |
| fact.name = f.name; |
| } |
| return fact; |
| }); |
| |
| updated = updated.concat(newTags); |
| this.editCard = null; |
| this.selectedSource.facts = updated; |
| this.updateFactSource(this.selectedSource); |
| }, |
| |
| deleteFactValue(trait, unique, value) { |
| this.selectedSource.facts = this.selectedSource.facts.filter((f) => { |
| return (f.value !== value); |
| }); |
| if (this.selectedSource.facts.filter((f) => f.trait === trait).length < 1) this.deleteFact(trait); // delete fact if it doesn't have any values |
| this.updateFactSource(this.selectedSource); |
| }, |
| |
| deleteFact(factTrait) { |
| this.selectedSource.facts = this.selectedSource.facts.filter((f) => f.trait !== factTrait); |
| this.updateFactSource(this.selectedSource); |
| }, |
| |
| updateRules(updatedRule) { |
| this.selectedSource.rules = this.selectedSource.rules.filter(match => match.trait !== updatedRule.trait); |
| this.editCard = ''; |
| this.addRule(updatedRule); |
| }, |
| |
| addRule(newRule) { |
| if (newRule.allow) { |
| const allowMatches = newRule.allow.map((m) => ({ |
| trait: newRule.trait, |
| match: m, |
| action: 'ALLOW' |
| })); |
| this.selectedSource.rules = this.selectedSource.rules.concat(allowMatches); |
| } |
| if (newRule.deny) { |
| const denyMatches = newRule.deny.map((m) => ({ |
| trait: newRule.trait, |
| match: m, |
| action: 'DENY' |
| })); |
| this.selectedSource.rules = this.selectedSource.rules.concat(denyMatches); |
| } |
| apiV2('PUT', `/api/v2/sources/${this.selectedSource.id}`, this.selectedSource).then(() => { |
| toast('Updated source', true); |
| this.getSources(); |
| this.addNewCard = false; |
| }).catch((error) => { |
| toast('Error adding rule', false); |
| console.error(error); |
| }); |
| }, |
| |
| deleteRule(trait) { |
| this.selectedSource.rules = this.selectedSource.rules.filter((r) => r.trait !== trait); |
| this.updateFactSource(); |
| }, |
| |
| updateRelationship(newRelationships) { |
| if (newRelationships) { |
| newRelationships.forEach((r) => this.addRelationship(r)); |
| } |
| }, |
| |
| addRelationship(newRelationship) { |
| if (!newRelationship.source.trait && !newRelationship.target.trait) return; |
| this.selectedSource.relationships.push(newRelationship); |
| apiV2('PUT', `/api/v2/sources/${this.selectedSource.id}`, this.selectedSource).then(() => { |
| toast('Updated source', true); |
| this.addNewCard = false; |
| this.getSources(); |
| }).catch((error) => { |
| toast('Error adding relationship', false); |
| console.error(error); |
| }); |
| }, |
| |
| deleteRelationship(trait) { |
| this.selectedSource.relationships = this.selectedSource.relationships.filter((r) => r.edge !== trait); |
| this.updateFactSource(); |
| }, |
| |
| deleteSource() { |
| apiV2('DELETE', `/api/v2/sources/${this.selectedSourceId}`).then((res) => { |
| toast('Deleted fact source', true); |
| this.selectedSourceId = ''; |
| this.selectedSource = { |
| name: '', |
| relationships: [] |
| }; |
| this.getSources(); |
| }).catch((error) => { |
| toast('Error deleting source', false); |
| console.error(error); |
| }); |
| }, |
| |
| saveSourceName() { |
| if (!this.selectedSource.name) { |
| toast('Source name cannot be blank', false); |
| return; |
| } |
| |
| apiV2('PATCH', `/api/v2/sources/${this.selectedSource.id}`, { name: this.selectedSource.name }).then(() => { |
| toast('Source name updated', true); |
| this.isEditingSourceName = false; |
| }).catch((error) => { |
| toast('Error loading page', false); |
| console.error(error); |
| }); |
| }, |
| |
| downloadSource() { |
| apiV2('GET', `/api/v2/sources/${this.selectedSourceId}`) |
| .then((res) => { |
| const dataURL = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(res, null, 2))}` |
| const fileName = `${this.selectedSource.name}.json` |
| const elem = document.createElement('a'); |
| elem.setAttribute('href', dataURL); |
| elem.setAttribute('download', fileName); |
| elem.click(); |
| elem.remove(); |
| }).catch((error) => { |
| toast('Error downloading source'); |
| console.error(error) |
| }); |
| }, |
| |
| resetSearch() { |
| this.abilitySearch = { |
| query: '', |
| tactic: '', |
| technique: null |
| }; |
| this.filterAbilities(); |
| }, |
| }; |
| } |
| |
| // # sourceURL=sources.js |
| </script> |
| |
| <style scoped> |
| #sourcePage .box.selectable:hover, #sourcePage .box.selected { |
| cursor: hand; |
| border: 2px solid white; |
| } |
| |
| #sourcePage .delete-card-button { |
| position: absolute; |
| top: 10px; |
| right: 16px; |
| } |
| |
| #sourcePage .sidebar { |
| min-height: calc(100vh - 230px); |
| border-right: 1px solid rgb(97, 97, 97); |
| } |
| |
| #sourcePage .hr { |
| content: ""; |
| height: 1px; |
| background-color: rgb(97, 97, 97); |
| margin: 0.5em 0 1em; |
| } |
| |
| #sourcePage button.no-style { |
| background-color: transparent; |
| border: none; |
| box-shadow: none; |
| } |
| |
| #sourcePage .button.no-style:focus, #sourcePage .button.no-style.is-focused { |
| color: white; |
| } |
| |
| @media (max-width: 900px) { |
| #sourcePage .box.sources { |
| width: 98%; |
| } |
| } |
| |
| @media (min-width: 900px) and (max-width: 1300px) { |
| #sourcePage .box.sources { |
| width: 48%; |
| } |
| } |
| |
| @media (min-width: 1300px) { |
| #sourcePage .box.sources { |
| width: 32%; |
| } |
| } |
| |
| #sourcePage .box.sources .field-body { |
| flex-grow: 4 !important; |
| flex-direction: column !important; |
| } |
| |
| #sourcePage .box.sources { |
| position: relative; |
| cursor: pointer; |
| border: 1px solid transparent; |
| background-color: #272727; |
| word-break: break-word; |
| } |
| |
| #sourcePage .box.sources:hover { |
| border: 1px solid #474747; |
| } |
| |
| #sourcePage .box.sources.add-new { |
| border: 2px solid #ffffff08; |
| } |
| |
| #sourcePage .box.sources.add-new:hover { |
| cursor: pointer; |
| background-color: #ffffff38; |
| } |
| |
| #sourcePage .editable-tag { |
| cursor: text !important; |
| } |
| |
| #sourcePage .break-anywhere { |
| word-break: break-word; |
| } |
| |
| #sourcePage .break-spaces, #sourcePage .button.is-small { |
| height: auto !important; |
| overflow-wrap: anywhere; |
| white-space: break-spaces !important; |
| min-height: 2.3em; |
| } |
| |
| #sourcePage .tag-input, #sourcePage .tag-number-input { |
| border: none; |
| background-color: transparent; |
| } |
| |
| #sourcePage .tag-number-input { |
| width: 2.5em; |
| } |
| |
| #sourcePage .w-110-px { |
| width: 110px; |
| } |
| |
| #sourcePage .w-50-px { |
| width: 50px !important; |
| } |
| |
| #sourcePage .w-35 { |
| width: 35% !important; |
| } |
| |
| #sourcePage .w-90 { |
| width: 90% !important; |
| } |
| |
| #sourcePage .w-100 { |
| width: 100% !important; |
| } |
| </style> |